본문으로 건너뛰기
cccskills
무료GitHub에 공개

mvcc-isolation-tests

Build per-database MVCC isolation-level tests - Read Uncommitted vs Read Committed vs Repeatable Read vs Serializable; verify which anomalies are prevented at each level (dirty read, non-repeatable read, phantom read, serialization anomaly, write skew). Per PostgreSQL transaction isolation docs; analogous patterns for MySQL InnoDB, SQL Server, and DynamoDB. Use when two concurrent transactions can touch the same rows (balance debit, seat booking, stock decrement), or before changing a service's default isolation level.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

testland/qa스타 13(GitHub 13 + cccskills 0)
연동 도구PostgreSQLMySQL

실행 점수 미검증

설치 방법 보기

포함된 파일(2)

  • SKILL.md11.3 KB
  • .tessl-plugin/plugin.json661 B

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

mvcc-isolation-tests

Per the PostgreSQL transaction isolation docs, the four standard SQL isolation levels permit different anomalies. Production code must run at the right level - and tests verify both that the chosen level prevents the anomalies the business cares about AND that the code copes when the level allows them.

When to use

  • Banking, inventory, booking - anywhere two transactions can conflict on the same data.
  • Choosing or auditing default isolation level for a new service.
  • Migration: changing isolation level (e.g., Read Committed → Repeatable Read) - tests verify nothing relies on old behavior.

Step 1 - Anomaly catalog

Per the PostgreSQL transaction isolation docs:

AnomalyWhat
Dirty readT2 reads uncommitted T1 changes
Non-repeatable readT1 reads X = 5; T2 commits X = 6; T1 re-reads X = 6
Phantom readT1 query returns 3 rows; T2 inserts row matching predicate; T1 re-runs query, gets 4 rows
Serialization anomalyConcurrent execution produces a result no serial order would
Write skewT1 reads {x, y} = {0, 0}; T2 reads same; both decide based on read-set; both commit; final state inconsistent

Step 2 - PostgreSQL isolation matrix

Per the PostgreSQL transaction isolation docs:

LevelDirtyNon-repeatablePhantomSerialization
Read Uncommitted (= Read Committed in PG)NOYESYESYES
Read CommittedNOYESYESYES
Repeatable ReadNONONO (PG strict)YES
SerializableNONONONO

PostgreSQL's Repeatable Read is stricter than SQL standard - it prevents phantoms via snapshot isolation. Other databases differ.

Step 3 - Test setup: two-connection harness

import psycopg
import threading
import time  # Steps 4-6 use time.sleep for the interleaving window

def two_connection_test(workload_a, workload_b, isolation="read committed"):
    conn_a = psycopg.connect("dbname=test", autocommit=False)
    conn_b = psycopg.connect("dbname=test", autocommit=False)

    # Set isolation level for both
    for c in (conn_a, conn_b):
        with c.cursor() as cur:
            cur.execute(f"SET TRANSACTION ISOLATION LEVEL {isolation}")

    barrier = threading.Barrier(2)
    results = {}

    def run(conn, work, key):
        barrier.wait()
        results[key] = work(conn)

    t_a = threading.Thread(target=run, args=(conn_a, workload_a, "a"))
    t_b = threading.Thread(target=run, args=(conn_b, workload_b, "b"))
    t_a.start(); t_b.start()
    t_a.join(); t_b.join()

    return results

The Steps 4-6 workloads use time.sleep(0.5) to open the interleaving window, which is timing-sensitive and can flake on slow CI. For a deterministic harness, replace the sleep with a second barrier (or a threading.Event) so the writer commits exactly between the reader's two reads, no wall-clock guess required:

# Deterministic variant: pass an extra Barrier(2) into both workloads.
# reader: read #1 -> gate.wait() -> read #2
# writer: gate.wait() -> UPDATE + commit       (writer races read #2)
# Use a pair of barriers (read1-done, write-done) if you need to pin the
# writer's commit strictly after read #1 and strictly before read #2.
gate = threading.Barrier(2)

Step 4 - Test for non-repeatable read at Read Committed

def test_non_repeatable_read_under_read_committed():
    setup_balance(account="acc1", balance=100)

    def reader(conn):
        with conn.cursor() as cur:
            cur.execute("SELECT balance FROM accounts WHERE id = 'acc1'")
            first = cur.fetchone()[0]
            time.sleep(0.5)  # let writer commit
            cur.execute("SELECT balance FROM accounts WHERE id = 'acc1'")
            second = cur.fetchone()[0]
        conn.commit()
        return (first, second)

    def writer(conn):
        with conn.cursor() as cur:
            cur.execute("UPDATE accounts SET balance = 200 WHERE id = 'acc1'")
        conn.commit()
        return None

    results = two_connection_test(reader, writer, isolation="read committed")
    first, second = results["a"]
    assert first == 100
    assert second == 200, "Read Committed permits non-repeatable read"

Step 5 - Test phantom read prevented at Repeatable Read (PG)

def test_phantom_read_prevented_under_repeatable_read():
    setup_orders(initial_count=3)

    def reader(conn):
        with conn.cursor() as cur:
            cur.execute("SELECT COUNT(*) FROM orders WHERE status = 'pending'")
            first_count = cur.fetchone()[0]
            time.sleep(0.5)
            cur.execute("SELECT COUNT(*) FROM orders WHERE status = 'pending'")
            second_count = cur.fetchone()[0]
        conn.commit()
        return (first_count, second_count)

    def writer(conn):
        with conn.cursor() as cur:
            cur.execute("INSERT INTO orders (status) VALUES ('pending')")
        conn.commit()
        return None

    results = two_connection_test(reader, writer, isolation="repeatable read")
    first, second = results["a"]
    assert first == second == 3, "PG Repeatable Read should prevent phantom"

Step 6 - Test write-skew at Repeatable Read (still possible!)

Per the PostgreSQL transaction isolation docs, write skew is a serialization anomaly that Repeatable Read does NOT prevent:

def test_write_skew_under_repeatable_read():
    """Two doctors on call; rule: at least one must remain on call.
    T1 reads {alice: on, bob: on}; decides safe to take alice off.
    T2 reads same; decides safe to take bob off.
    Both commit. Now nobody on call → anomaly."""
    setup_doctors([("alice", "on_call"), ("bob", "on_call")])

    def take_alice_off(conn):
        with conn.cursor() as cur:
            cur.execute("SELECT COUNT(*) FROM doctors WHERE status = 'on_call'")
            on_call_count = cur.fetchone()[0]
            if on_call_count >= 2:
                cur.execute("UPDATE doctors SET status = 'off' WHERE name = 'alice'")
        conn.commit()

    def take_bob_off(conn):
        with conn.cursor() as cur:
            cur.execute("SELECT COUNT(*) FROM doctors WHERE status = 'on_call'")
            on_call_count = cur.fetchone()[0]
            if on_call_count >= 2:
                cur.execute("UPDATE doctors SET status = 'off' WHERE name = 'bob'")
        conn.commit()

    two_connection_test(take_alice_off, take_bob_off, isolation="repeatable read")

    on_call_after = count_on_call()
    # Under Repeatable Read, write skew is possible: both txns read 2
    # on-call, both flip one off, and the final state is 0 on call (the
    # anomaly). A correctly-serialized run would leave 1. Either value
    # proves RR does NOT serialize this pair, so both are accepted here.
    # (Run the same workload at Serializable - Step 7 - to see one txn abort.)
    assert on_call_after in (0, 1)

Step 7 - Test Serializable rolls back conflicting transactions

def test_serializable_aborts_one_of_skew_pair():
    setup_doctors([("alice", "on_call"), ("bob", "on_call")])

    aborted = [0]
    def take_off(name, conn):
        try:
            with conn.cursor() as cur:
                cur.execute("SELECT COUNT(*) FROM doctors WHERE status = 'on_call'")
                if cur.fetchone()[0] >= 2:
                    cur.execute("UPDATE doctors SET status = 'off' WHERE name = %s", (name,))
            conn.commit()
        except psycopg.errors.SerializationFailure:
            conn.rollback()
            aborted[0] += 1

    two_connection_test(
        lambda c: take_off("alice", c),
        lambda c: take_off("bob", c),
        isolation="serializable",
    )

    assert aborted[0] >= 1, "Serializable must abort at least one"
    assert count_on_call() >= 1, "At least one doctor still on call"

Per the PostgreSQL transaction isolation docs: Serializable raises could not serialize access due to read/write dependencies among transactions - application must catch + retry.

Step 8 - Per-database differences

DBDefaultRR prevents phantoms?Serializable cost
PostgreSQLRead CommittedYes (snapshot isolation)Predicate locks; can be expensive
MySQL InnoDBRepeatable ReadYes (gap locks)Default RR is "good enough" for most
SQL ServerRead CommittedNo (default), Yes with READ_COMMITTED_SNAPSHOTSnapshot isolation available
OracleRead CommittedYes via Read ConsistencySerializable via lock
DynamoDBEventually consistent readsStrongly consistent reads opt-inTransactions: ACID with TransactWriteItems
MongoDBSnapshot read concernMulti-doc txn (4.0+) ACIDLimit on doc size (16MB / txn)

Test the actual DB you ship with - defaults differ.

Step 9 - Application-level retry pattern

def with_serializable_retry(fn, max_retries=3):
    for attempt in range(max_retries):
        try:
            with conn.cursor() as cur:
                cur.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
            result = fn()
            conn.commit()
            return result
        except psycopg.errors.SerializationFailure:
            conn.rollback()
            if attempt == max_retries - 1:
                raise
            time.sleep(0.01 * (2 ** attempt))

Test the retry logic itself:

def test_serializable_retry_completes():
    # Simulate concurrent conflict; verify retry succeeds eventually
    ...

Anti-patterns

Anti-patternWhy it failsFix
Run at default isolation, hope for the bestAnomaly hits in prodChoose level deliberately + test
Test only happy-path single-transactionConcurrency bugs need 2-conn harnessStep 3 harness
Use Serializable everywhere "to be safe"Performance cliff; retries swampTest perf at chosen level
Skip retry logic for SerializableProduction transaction errors leakStep 9 retry pattern
Test PG and assume MySQL behaves sameRR semantics differ (gap locks vs snapshot)Per-DB tests (Step 8)

Limitations

  • Test fixture flakiness: timing-sensitive; may need barrier adjustment per machine speed.
  • Eventually-consistent NoSQL stores need different tests (probabilistic + windowed).
  • ORM abstractions can hide explicit SET ISOLATION LEVEL - verify the actual SQL emitted.

References

  • PostgreSQL transaction isolation docs - anomaly definitions, per-level matrix, retry patterns
  • MySQL InnoDB isolation - dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html
  • A Critique of ANSI SQL Isolation Levels (Berenson 1995) - seminal paper on isolation levels
  • race-condition-test-author, deadlock-detection-harness - in-process concurrency siblings

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Builds a CI gate that fails the build on **new** WCAG / a11y violations introduced by a PR while grandfathering pre-existing violations on a per-rule / per-page baseline. Aggregates verdicts from axe-core / pa11y / Lighthouse a11y / WAVE / IBM Equal Access scans. Use when a project has accumulated a11y debt and a strict "zero violations" gate would block every PR - the ratchet pattern lets the team ship while preventing regressions.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

testland/qa132026년 10월 20일 업데이트

Workflow skill that builds an A/B-test validity checklist from an experiment proposal, walking the canonical design-correctness gates - pre-registered OEC/power/guardrails, randomization unit + SRM check, assignment integrity, telemetry, peeking discipline, novelty/primacy, post-experiment SRM re-check - into a per-experiment checklist + sign-off form. Use when launching, auditing, or governing an experiment. For pitfall mechanics (guardrails, peeking) see experiment-results-interpreter's references; to read an already-valid result use experiment-results-interpreter; for per-SDK harness tests use experiment-sdk-testing - this gates DESIGN, not SDK code.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

testland/qa132026년 10월 20일 업데이트

The single install-flow skill: the reference contract for the PWA install flow (installability gate fields, `beforeinstallprompt` handshake, per-platform paths - Android WebAPK / iOS Share menu / Firefox no-op - and the `display-mode` post-install signal, in references/install-flow-reference.md) plus the build-an-X workflow that emits the Add-to-Home-Screen suite. Walks the four-stage timeline, emitting one test per gate cell per [web.dev/articles/install-criteria][install-criteria], the deferred-prompt → `prompt()` → `userChoice` chain per [web.dev/articles/customize-install][customize-install], the iOS Safari manual-metadata branch (`apple-touch-icon`) per [web.dev/learn/pwa/installation][learn-pwa], and the post-install `(display-mode: standalone)` MQ assertion. Output: a Playwright spec with per-stage cells plus a coverage matrix. Use when a PWA's manifest, icons, or install handler change, when install conversion drops at an unknown stage, or when triaging a flaky install assertion.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

testland/qa132026년 10월 20일 업데이트

Build-an-X workflow that uses an LLM to map existing tests to spec sections - given a spec doc + the test suite, the LLM identifies which tests cover which sections, surfaces uncovered sections (gap), and recommends specific tests to add. Output is a coverage matrix per spec ID. Scope is mapping tests that already exist and naming the gaps, not authoring tests for new acceptance criteria. Use when a spec doc and a test suite both exist but nobody can say which requirements are actually covered - before a release sign-off, an audit, or a decision about where to spend the next round of test effort.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

testland/qa132026년 10월 20일 업데이트

Generates tests from natural-language specs (acceptance criteria, user stories, requirements) using an LLM, with confidence scoring per test case (LLM self-assessment plus heuristics: assertion quality, naming, completeness), batching uncertain cases for human review, and integration with the team's existing test framework. Use when the user asks to generate unit tests from acceptance criteria, convert user stories to test cases, automate test creation from requirements, or augment a spec-driven test suite with AI-generated stubs that are then curated before merge.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

testland/qa132026년 10월 20일 업데이트

Configures Allure Report (test-runner adapter install, `allure-results` directory wiring, `categories.json` for failure classification, `history-trend.json` retention via the copy-history-between-runs pattern), runs the Allure CLI to convert `allure-results` to a static HTML site, and uploads the report as a CI artifact. Use when the team needs richer test reporting than JUnit XML - step-level attachments, per-test history, retry tracking, and severity / epic / feature labeling across framework-agnostic adapters (pytest, Jest, JUnit, TestNG, NUnit, Mocha). As a rich static HTML report generator, it is the open-source alternative to the sunset ExtentReports (JVM/.NET per-test HTML narrative); for hosted cross-run flakiness analytics rather than a static per-run report use currents-integration.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

testland/qa132026년 10월 20일 업데이트

testland의 스킬 모두 보기

이 스킬의 문제 신고