-
Notifications
You must be signed in to change notification settings - Fork 61
Fix CREATE TYPE race that can crash the worker on first boot #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b83740b
0947e04
dfa8ef7
d39c918
f5f7ff8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||||||||||||||||||||||||||||||
| import threading | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import sqlalchemy | ||||||||||||||||||||||||||||||||||
| from osprey.worker.lib.singletons import CONFIG | ||||||||||||||||||||||||||||||||||
| from osprey.worker.lib.storage import postgres | ||||||||||||||||||||||||||||||||||
| from psycopg2.errors import DuplicateDatabase, InvalidCatalogName | ||||||||||||||||||||||||||||||||||
| from sqlalchemy.exc import ProgrammingError | ||||||||||||||||||||||||||||||||||
| from sqlalchemy_utils import create_database, drop_database | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def test_create_schema_survives_concurrent_callers_on_a_fresh_database(): | ||||||||||||||||||||||||||||||||||
| """Regression test for issue #432: on a fresh database, two processes (e.g. the worker and | ||||||||||||||||||||||||||||||||||
| the UI API) both calling create_schema() at startup used to be able to race on `CREATE TYPE` | ||||||||||||||||||||||||||||||||||
| for the job_status enum, since SQLAlchemy's enum creation is check-then-create rather than | ||||||||||||||||||||||||||||||||||
| atomic. Both would see the type as missing, both would issue CREATE TYPE, and the loser would | ||||||||||||||||||||||||||||||||||
| crash with a UniqueViolation on `pg_type_typname_nsp_index`. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Simulate two racing processes with two independent engines hitting a brand new database at | ||||||||||||||||||||||||||||||||||
| the same time, and assert neither raises. | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| base_url = CONFIG.instance()['POSTGRES_HOSTS']['osprey_db'] | ||||||||||||||||||||||||||||||||||
| fresh_url = base_url.rsplit('/', 1)[0] + '/osprey_test_concurrent_schema_create' | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| drop_database(fresh_url) | ||||||||||||||||||||||||||||||||||
| except ProgrammingError as e: | ||||||||||||||||||||||||||||||||||
| if not isinstance(e.orig, InvalidCatalogName): | ||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| create_database(fresh_url) | ||||||||||||||||||||||||||||||||||
| except ProgrammingError as e: | ||||||||||||||||||||||||||||||||||
| if not isinstance(e.orig, DuplicateDatabase): | ||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| errors: list[Exception] = [] | ||||||||||||||||||||||||||||||||||
| barrier = threading.Barrier(2) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _create_schema() -> None: | ||||||||||||||||||||||||||||||||||
| engine = sqlalchemy.create_engine(fresh_url) | ||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| barrier.wait(timeout=5) | ||||||||||||||||||||||||||||||||||
| postgres.create_schema(engine) | ||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||
| errors.append(e) | ||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||
| engine.dispose() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| threads = [threading.Thread(target=_create_schema) for _ in range(2)] | ||||||||||||||||||||||||||||||||||
| for thread in threads: | ||||||||||||||||||||||||||||||||||
| thread.start() | ||||||||||||||||||||||||||||||||||
| for thread in threads: | ||||||||||||||||||||||||||||||||||
| thread.join(timeout=10) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| assert not errors, errors | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+49
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Fail the test explicitly if a thread times out. In Python, Checking 🐛 Proposed fix to handle thread timeouts threads = [threading.Thread(target=_create_schema) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join(timeout=10)
+ if thread.is_alive():
+ raise TimeoutError("Schema creation thread timed out and may be deadlocked")
assert not errors, errors📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good suggestion. Let's add this check. Otherwise test LGTM!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐇
You are interacting with an AI system. |
||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||
| drop_database(fresh_url) | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make this snappier, something like: