Skip to content

fix(test): resolve parallel E2E database locks and timeouts#271

Merged
DogukanUrker merged 1 commit into
mainfrom
fix/e2e-parallel-concurrency
Jun 27, 2026
Merged

fix(test): resolve parallel E2E database locks and timeouts#271
DogukanUrker merged 1 commit into
mainfrom
fix/e2e-parallel-concurrency

Conversation

@DogukanUrker

@DogukanUrker DogukanUrker commented Jun 27, 2026

Copy link
Copy Markdown
Owner

This PR fixes database locking and transient timeouts in parallel E2E test execution.

Summary by CodeRabbit

  • New Features

    • Improved SQLite reliability for longer lock wait times and WAL usage.
    • End-to-end tests now run against a dedicated SQLite database with a safer setup/teardown flow.
  • Bug Fixes

    • Default admin password handling now stays consistent with the configured default password.
    • Redirect responses after login, post deletion, and comment actions are simplified to avoid explicit 301 status codes.
  • Chores

    • Updated ignore rules to exclude temporary SQLite database and journal/WAL files.
    • Enhanced e2e test stability by adjusting navigation/wait timing and transactional cleanup behavior.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@DogukanUrker, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 31 minutes and 50 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4246334a-de6c-4c57-b8d4-13824d9cb96e

📥 Commits

Reviewing files that changed from the base of the PR and between f4fd21f and 1ceb798.

📒 Files selected for processing (7)
  • .gitignore
  • app/database.py
  • app/routes/login.py
  • app/routes/post.py
  • tests/e2e/conftest.py
  • tests/e2e/helpers/database_helpers.py
  • tests/e2e/post/test_post.py
📝 Walkthrough

Walkthrough

Adds SQLite WAL and timeout settings in the app, refreshes the stored default-admin password hash when needed, removes explicit 301 redirect tuples, and reworks E2E SQLite test database setup, cleanup, and helper transactions for isolated parallel runs.

Changes

App: SQLite pragmas and redirect fixes

Layer / File(s) Summary
SQLite PRAGMA and admin password sync in database.py
app/database.py
Adds SQLite connect-time busy timeout, SQLite connection timeout, WAL startup mode, and default-admin password hash refresh behavior.
Remove explicit 301 status from login and post redirects
app/routes/login.py, app/routes/post.py
Redirect responses now return plain redirect() calls, and the comment redirect drops the slug parameter.

E2E: parallel-safe test database lifecycle

Layer / File(s) Summary
Test DB fixture and session cleanup (conftest.py)
tests/e2e/conftest.py, .gitignore
Switches E2E tests to flaskblog_test.db, patches Playwright waits, replaces backup/restore with locked test DB setup and teardown, updates session cleanup, increases authenticated-page wait time, and ignores SQLite test DB artifacts.
SQLite helper transaction hardening (database_helpers.py)
tests/e2e/helpers/database_helpers.py
Wraps SQLite helper operations in explicit transactions, updates connection settings, and returns inserted row ids from helper inserts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • DogukanUrker/FlaskBlog#220: Shares the same E2E auth and SQLite test infrastructure touched by the updated fixture and helper code.

Poem

🐇 I hopped through WAL with tidy feet,
Test DBs now dance in a separate seat.
No 301s to slow my run,
Just commits and redirects when work is done.
The rabbit grins as the green checks bloom,
In flaskblog_test.db, there’s plenty of room.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is too brief and does not follow the required template sections or include the issue reference and proposed changes. Add the required 'Fixes #' line and a 'Proposed Changes' section with concise bullet points describing the main updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: fixing parallel E2E database lock and timeout issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/e2e-parallel-concurrency

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DogukanUrker DogukanUrker force-pushed the fix/e2e-parallel-concurrency branch from 80a297a to f4fd21f Compare June 27, 2026 22:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e/conftest.py`:
- Around line 218-231: The non-xdist teardown in conftest leaves stale setup
state because it removes the database files but does not clear the
DB_BACKUP_DONE sentinel or the .locks artifacts. Update the teardown path that
handles the session DB cleanup to also delete DB_BACKUP_DONE and any lock files,
so the setup logic in the DB_BACKUP_DONE.exists() / shutil.copy2 block can
recreate a fresh database on the next local run.
- Around line 42-45: The e2e test setup still points all xdist workers at the
same sqlite file, which makes parallel runs interfere with each other. Update
the database setup in conftest.py to derive the test DB filename from the
current worker id and create a separate copy per worker, and make sure any
db_path/reset flow in tests/e2e/helpers/database_helpers.py uses that
worker-specific path instead of the shared TEST_DB_NAME. Keep the logic
centralized around the existing SQLALCHEMY_DATABASE_URI setup and reset_database
helper so each worker gets isolated fixtures.
- Around line 225-230: The WAL initialization in the SQLite setup block is
swallowing all exceptions, which hides failures in `sqlite3.connect`/`PRAGMA
journal_mode=WAL` and disables the intended lock-timeout behavior. Update the
setup around the `conn = sqlite3.connect(...)` and `conn.execute("PRAGMA
journal_mode=WAL;")` flow to fail fast or at minimum log/raise the exception
instead of using a bare `except Exception: pass`, so any WAL setup problem is
surfaced during test startup.

In `@tests/e2e/helpers/database_helpers.py`:
- Around line 348-353: The INSERT in database_helpers.py is writing time_stamp
with int(time.time()) instead of the shared timestamp helper, which violates the
Python timestamp contract. Update the comments insert path to use the
Unix-integer helper from utils/time.py rather than calling time.time() directly,
keeping the change localized to the database_helpers helper that builds the
comments row.
- Around line 89-90: Move the slow password hashing in the database helper
before the sqlite write lock is acquired: in the helper around
conn.execute("BEGIN IMMEDIATE") and cursor = conn.cursor(), compute the
sha512_crypt.hash() result first, then begin the transaction and write the row.
This keeps parallel workers from holding the write lock during CPU-bound work
and reduces busy-timeout failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54339a55-b717-4c8a-946f-12a147a95d2f

📥 Commits

Reviewing files that changed from the base of the PR and between 9ddefb8 and 80a297a.

📒 Files selected for processing (7)
  • .gitignore
  • app/database.py
  • app/routes/login.py
  • app/routes/post.py
  • tests/e2e/conftest.py
  • tests/e2e/helpers/database_helpers.py
  • tests/e2e/post/test_post.py

Comment thread tests/e2e/conftest.py
Comment on lines +42 to +45
# Force tests to use a temporary test database copy to keep git workspace clean
TEST_DB_NAME = "flaskblog_test.db"
os.environ["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{TEST_DB_NAME}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Use a per-worker test database.

TEST_DB_NAME/db_path still sends every xdist worker to the same sqlite file. Cross-file, reset_database() in tests/e2e/helpers/database_helpers.py wipes shared tables, so one worker can erase another worker's fixtures mid-test and parallel runs stay nondeterministic. Please derive the DB filename from the worker id and provision one copy per worker instead of one shared file.

Also applies to: 82-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/conftest.py` around lines 42 - 45, The e2e test setup still points
all xdist workers at the same sqlite file, which makes parallel runs interfere
with each other. Update the database setup in conftest.py to derive the test DB
filename from the current worker id and create a separate copy per worker, and
make sure any db_path/reset flow in tests/e2e/helpers/database_helpers.py uses
that worker-specific path instead of the shared TEST_DB_NAME. Keep the logic
centralized around the existing SQLALCHEMY_DATABASE_URI setup and reset_database
helper so each worker gets isolated fixtures.

Comment thread tests/e2e/conftest.py
Comment on lines 218 to 231
if not DB_BACKUP_DONE.exists():
# First worker to acquire lock does the backup
backup_path = db_path.with_suffix(".db.bak")
if db_path.exists():
shutil.copy2(db_path, backup_path)
# Copy original database to the test database file
if orig_db_path.exists():
shutil.copy2(orig_db_path, db_path)
# Ensure the test database has WAL mode enabled once
import sqlite3

try:
conn = sqlite3.connect(str(db_path))
conn.execute("PRAGMA journal_mode=WAL;")
conn.close()
except Exception:
pass
DB_BACKUP_DONE.touch()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Non-xdist teardown leaves stale setup state behind.

This branch deletes the DB files but never clears the DB_BACKUP_DONE sentinel or .locks. On the next local run, Line 218 skips the copy step because the sentinel still exists, so the session can start from a missing or stale database.

Suggested fix
     if not _is_xdist_worker(request.config):
         for suffix in ["", "-wal", "-shm"]:
             path = Path(str(db_path) + suffix)
             path.unlink(missing_ok=True)
+        DB_BACKUP_DONE.unlink(missing_ok=True)
+        shutil.rmtree(LOCK_DIR, ignore_errors=True)

Also applies to: 235-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/conftest.py` around lines 218 - 231, The non-xdist teardown in
conftest leaves stale setup state because it removes the database files but does
not clear the DB_BACKUP_DONE sentinel or the .locks artifacts. Update the
teardown path that handles the session DB cleanup to also delete DB_BACKUP_DONE
and any lock files, so the setup logic in the DB_BACKUP_DONE.exists() /
shutil.copy2 block can recreate a fresh database on the next local run.

Comment thread tests/e2e/conftest.py
Comment on lines +225 to +230
try:
conn = sqlite3.connect(str(db_path))
conn.execute("PRAGMA journal_mode=WAL;")
conn.close()
except Exception:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don't swallow WAL setup failures.

If PRAGMA journal_mode=WAL fails here, the suite silently falls back to default journaling and the lock-timeout fix is effectively disabled. This should fail fast or at least surface the error.

Suggested fix
-                try:
-                    conn = sqlite3.connect(str(db_path))
-                    conn.execute("PRAGMA journal_mode=WAL;")
-                    conn.close()
-                except Exception:
-                    pass
+                with sqlite3.connect(str(db_path)) as conn:
+                    mode = conn.execute("PRAGMA journal_mode=WAL;").fetchone()[0]
+                if str(mode).lower() != "wal":
+                    raise RuntimeError(
+                        f"Failed to enable WAL for {db_path}: got {mode!r}"
+                    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
conn = sqlite3.connect(str(db_path))
conn.execute("PRAGMA journal_mode=WAL;")
conn.close()
except Exception:
pass
with sqlite3.connect(str(db_path)) as conn:
mode = conn.execute("PRAGMA journal_mode=WAL;").fetchone()[0]
if str(mode).lower() != "wal":
raise RuntimeError(
f"Failed to enable WAL for {db_path}: got {mode!r}"
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/conftest.py` around lines 225 - 230, The WAL initialization in the
SQLite setup block is swallowing all exceptions, which hides failures in
`sqlite3.connect`/`PRAGMA journal_mode=WAL` and disables the intended
lock-timeout behavior. Update the setup around the `conn = sqlite3.connect(...)`
and `conn.execute("PRAGMA journal_mode=WAL;")` flow to fail fast or at minimum
log/raise the exception instead of using a bare `except Exception: pass`, so any
WAL setup problem is surfaced during test startup.

Comment thread tests/e2e/helpers/database_helpers.py
Comment on lines 348 to 353
cursor.execute(
"""
INSERT INTO comments (post_id, comment, username, time_stamp)
VALUES (?, ?, ?, ?)
""",
(post_id, comment, username, int(time.time())),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use utils/time.py for this timestamp write.

This stores time_stamp via int(time.time()) instead of the shared helper, which breaks the repository's timestamp contract for Python files. As per coding guidelines, **/*.py: Timestamps must be stored as Unix integers via utils/time.py.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/helpers/database_helpers.py` around lines 348 - 353, The INSERT in
database_helpers.py is writing time_stamp with int(time.time()) instead of the
shared timestamp helper, which violates the Python timestamp contract. Update
the comments insert path to use the Unix-integer helper from utils/time.py
rather than calling time.time() directly, keeping the change localized to the
database_helpers helper that builds the comments row.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/routes/post.py`:
- Line 84: The redirect after a successful comment submit should go directly to
the canonical post URL instead of the non-canonical url_id-only route. In the
post.post flow, use the already available post_slug together with url_id when
building the url_for target so the redirect lands on the final slugged URL
without an extra round trip.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a914d2b7-3a26-4a20-b6c2-8a6a43e4e63d

📥 Commits

Reviewing files that changed from the base of the PR and between 80a297a and f4fd21f.

📒 Files selected for processing (7)
  • .gitignore
  • app/database.py
  • app/routes/login.py
  • app/routes/post.py
  • tests/e2e/conftest.py
  • tests/e2e/helpers/database_helpers.py
  • tests/e2e/post/test_post.py
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/e2e/post/test_post.py
  • app/routes/login.py
  • tests/e2e/conftest.py
  • tests/e2e/helpers/database_helpers.py
  • app/database.py

Comment thread app/routes/post.py
)

return redirect(url_for("post.post", url_id=url_id)), 301
return redirect(url_for("post.post", url_id=url_id))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Redirect straight to the canonical post URL.

Line 84 drops slug even though post_slug is already available, so every successful comment submit now incurs an extra /post/<url_id>/post/<slug>-<url_id> round trip before the page reloads. This still works, but it adds avoidable latency on a flow this PR is trying to make less timeout-prone.

Suggested fix
-            return redirect(url_for("post.post", url_id=url_id))
+            return redirect(url_for("post.post", url_id=url_id, slug=post_slug))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return redirect(url_for("post.post", url_id=url_id))
return redirect(url_for("post.post", url_id=url_id, slug=post_slug))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/routes/post.py` at line 84, The redirect after a successful comment
submit should go directly to the canonical post URL instead of the non-canonical
url_id-only route. In the post.post flow, use the already available post_slug
together with url_id when building the url_for target so the redirect lands on
the final slugged URL without an extra round trip.

@DogukanUrker DogukanUrker force-pushed the fix/e2e-parallel-concurrency branch 3 times, most recently from 36c3718 to 5948467 Compare June 27, 2026 22:32
@DogukanUrker DogukanUrker force-pushed the fix/e2e-parallel-concurrency branch from 5948467 to 1ceb798 Compare June 27, 2026 22:35
@DogukanUrker DogukanUrker merged commit 0d112bc into main Jun 27, 2026
3 checks passed
@DogukanUrker DogukanUrker deleted the fix/e2e-parallel-concurrency branch June 27, 2026 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant