The scenario: a hospital database has two on-call doctors, Alice and Bob. The rule is that at least one doctor must be on call at all times. Both doctors check the database at the same moment and see that two doctors are on call — it looks safe for one to leave. They each submit their own transaction to go off call simultaneously.
Under snapshot isolation each transaction reads from its own frozen snapshot — so each sees both doctors still on call when it decides. Both transactions commit successfully. The final state: zero doctors on call. The rule is violated.
<div class="scenario">
<div class="rule-box" id="ruleBox">
Rule: at least 1 doctor must be on call
</div>
<div class="db-state">
<div class="doctor-card" id="cardAlice">
<div class="doc-name">Alice</div>
<div class="doc-status" id="statusAlice">on call</div>
</div>
<div class="doctor-card" id="cardBob">
<div class="doc-name">Bob</div>
<div class="doc-status" id="statusBob">on call</div>
</div>
</div>
<div class="txns">
<div class="txn" id="txnA">
<div class="txn-title">Transaction A (Alice)</div>
<div class="txn-log" id="logA"></div>
<button id="btnA" type="button">Alice goes off call</button>
</div>
<div class="txn" id="txnB">
<div class="txn-title">Transaction B (Bob)</div>
<div class="txn-log" id="logB"></div>
<button id="btnB" type="button">Bob goes off call</button>
</div>
</div>
<div class="result-box" id="resultBox"></div>
<div class="btns">
<button id="btnRun" type="button" class="primary">Run both simultaneously</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #1a1a2e; background: #f8f9fc; padding: 12px; }
.scenario { display: flex; flex-direction: column; gap: 10px; }
.rule-box { background: #e8f4fd; border: 1.5px solid #3a86d4; border-radius: 8px; padding: 8px 12px; font-weight: 600; color: #1a5296; font-size: 13px; }
.rule-box.violated { background: #fde8e8; border-color: #d9534f; color: #a02020; }
.rule-box.ok { background: #e8fdf0; border-color: #28a745; color: #155724; }
.db-state { display: flex; gap: 10px; }
.doctor-card { flex: 1; background: #fff; border: 1.5px solid #c8d6e5; border-radius: 10px; padding: 10px 12px; text-align: center; }
.doctor-card.off { opacity: 0.45; }
.doc-name { font-weight: 700; font-size: 15px; margin-bottom: 4px; }
.doc-status { font-size: 12px; background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; display: inline-block; }
.doctor-card.off .doc-status { background: #f5c6cb; color: #721c24; }
.txns { display: flex; gap: 10px; }
.txn { flex: 1; background: #fff; border: 1.5px solid #c8d6e5; border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
.txn-title { font-weight: 700; font-size: 12px; color: #555; text-transform: uppercase; letter-spacing: .04em; }
.txn-log { min-height: 72px; font-size: 11.5px; color: #333; line-height: 1.55; }
.txn-log .step { padding: 1px 0; }
.txn-log .step.read { color: #1a5296; }
.txn-log .step.write { color: #c55a00; }
.txn-log .step.commit { color: #155724; font-weight: 700; }
.txn button { font: 600 12px system-ui; padding: 5px 10px; border-radius: 7px;
border: 1.5px solid #1d3557; background: #1d3557; color: #fff; cursor: pointer; }
.txn button:disabled { opacity: .4; cursor: not-allowed; }
.result-box { min-height: 32px; padding: 8px 12px; border-radius: 8px; font-weight: 700;
font-size: 13px; text-align: center; display: none; }
.result-box.violation { display: block; background: #fde8e8; color: #a02020; border: 1.5px solid #d9534f; }
.result-box.safe { display: block; background: #e8fdf0; color: #155724; border: 1.5px solid #28a745; }
.btns { display: flex; gap: 8px; flex-wrap: wrap; }
button.primary { font: 700 13px system-ui; padding: 7px 16px; border-radius: 8px;
border: 1.5px solid #1d3557; background: #1d3557; color: #fff; cursor: pointer; }
button.ghost { font: 600 13px system-ui; padding: 7px 14px; border-radius: 8px;
border: 1.5px solid #1d3557; background: #fff; color: #1d3557; cursor: pointer; }
button:disabled { opacity: .4; cursor: not-allowed; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Snapshot Isolation write-skew demo.
// Each transaction reads a consistent snapshot at its start time, decides
// it's safe to go off-call (because the snapshot shows 2 doctors on call),
// then writes. Both commit. Final state: 0 doctors on call — the constraint
// "at least 1 on call" is violated. This is write skew: two valid snapshots produce an impossible combined result.
const state = { alice: true, bob: true }; // true = on call
let running = false;
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
function log(id, text, cls) {
const el = document.getElementById(id);
const span = document.createElement('div');
span.className = 'step ' + (cls || '');
span.textContent = text;
el.appendChild(span);
}
function updateCards() {
const ca = document.getElementById('cardAlice');
const cb = document.getElementById('cardBob');
const sa = document.getElementById('statusAlice');
const sb = document.getElementById('statusBob');
ca.className = 'doctor-card' + (state.alice ? '' : ' off');
cb.className = 'doctor-card' + (state.bob ? '' : ' off');
sa.textContent = state.alice ? 'on call' : 'off call';
sb.textContent = state.bob ? 'on call' : 'off call';
}
function reset() {
state.alice = true; state.bob = true; running = false;
document.getElementById('logA').innerHTML = '';
document.getElementById('logB').innerHTML = '';
document.getElementById('resultBox').className = 'result-box';
document.getElementById('resultBox').textContent = '';
document.getElementById('ruleBox').className = 'rule-box';
document.getElementById('btnA').disabled = false;
document.getElementById('btnB').disabled = false;
document.getElementById('btnRun').disabled = false;
updateCards();
console.log('[reset] state:', JSON.stringify(state));
}
async function runTxnA(snapshot) {
const onCall = (snapshot.alice ? 1 : 0) + (snapshot.bob ? 1 : 0);
log('logA', fmt('READ snapshot: alice=%alice%, bob=%bob%', { alice: snapshot.alice, bob: snapshot.bob }), 'read');
log('logA', fmt('Count on call = %onCall% ≥ 1, safe to leave', { onCall: onCall }), 'read');
await delay(600);
log('logA', 'WRITE alice = false', 'write');
state.alice = false;
await delay(300);
log('logA', 'COMMIT ✓', 'commit');
updateCards();
console.log('[txnA] committed. alice=false (snapshot showed', onCall, 'on call)');
}
async function runTxnB(snapshot) {
const onCall = (snapshot.alice ? 1 : 0) + (snapshot.bob ? 1 : 0);
log('logB', fmt('READ snapshot: alice=%alice%, bob=%bob%', { alice: snapshot.alice, bob: snapshot.bob }), 'read');
log('logB', fmt('Count on call = %onCall% ≥ 1, safe to leave', { onCall: onCall }), 'read');
await delay(700);
log('logB', 'WRITE bob = false', 'write');
state.bob = false;
await delay(200);
log('logB', 'COMMIT ✓', 'commit');
updateCards();
console.log('[txnB] committed. bob=false (snapshot showed', onCall, 'on call)');
}
async function runSimultaneous() {
if (running) return;
running = true;
document.getElementById('btnRun').disabled = true;
document.getElementById('btnA').disabled = true;
document.getElementById('btnB').disabled = true;
document.getElementById('logA').innerHTML = '';
document.getElementById('logB').innerHTML = '';
document.getElementById('resultBox').className = 'result-box';
// Both transactions take a snapshot of the CURRENT committed state simultaneously
const snapA = { alice: state.alice, bob: state.bob };
const snapB = { alice: state.alice, bob: state.bob };
console.log('[snapshot] both txns read:', JSON.stringify(snapA));
// Run both transactions concurrently
await Promise.all([runTxnA(snapA), runTxnB(snapB)]);
// Check the constraint
const onCallNow = (state.alice ? 1 : 0) + (state.bob ? 1 : 0);
console.log('[result] doctors on call:', onCallNow, '| state:', JSON.stringify(state));
const rb = document.getElementById('resultBox');
const rule = document.getElementById('ruleBox');
if (onCallNow === 0) {
rb.textContent = 'Write skew! 0 doctors on call — the constraint is violated.';
rb.className = 'result-box violation';
rule.className = 'rule-box violated';
console.log('[write-skew] CONFIRMED: both txns saw 2 on call; both went off; constraint broken.');
} else {
rb.textContent = fmt('Constraint holds: %onCallNow% doctor(s) on call.', { onCallNow: onCallNow });
rb.className = 'result-box safe';
rule.className = 'rule-box ok';
}
}
async function runSingleA() {
if (running) return;
running = true;
document.getElementById('btnA').disabled = true;
document.getElementById('btnRun').disabled = true;
const snap = { alice: state.alice, bob: state.bob };
await runTxnA(snap);
const ok = (state.alice ? 1 : 0) + (state.bob ? 1 : 0) >= 1;
const rb = document.getElementById('resultBox');
rb.textContent = ok ? 'Constraint holds: Alice off call, Bob still on.' : 'Violation!';
rb.className = 'result-box ' + (ok ? 'safe' : 'violation');
running = false;
}
async function runSingleB() {
if (running) return;
running = true;
document.getElementById('btnB').disabled = true;
document.getElementById('btnRun').disabled = true;
const snap = { alice: state.alice, bob: state.bob };
await runTxnB(snap);
const ok = (state.alice ? 1 : 0) + (state.bob ? 1 : 0) >= 1;
const rb = document.getElementById('resultBox');
rb.textContent = ok ? 'Constraint holds: Bob off call, Alice still on.' : 'Violation!';
rb.className = 'result-box ' + (ok ? 'safe' : 'violation');
running = false;
}
document.getElementById('btnRun').addEventListener('click', runSimultaneous);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('btnA').addEventListener('click', runSingleA);
document.getElementById('btnB').addEventListener('click', runSingleB);
// Initial render
updateCards();
console.log('[init] state:', JSON.stringify(state));
<div class="scenario">
<div class="rule-box" id="ruleBox">
Rule: at least 1 doctor must be on call
</div>
<div class="db-state">
<div class="doctor-card" id="cardAlice">
<div class="doc-name">Alice</div>
<div class="doc-status" id="statusAlice">on call</div>
</div>
<div class="doctor-card" id="cardBob">
<div class="doc-name">Bob</div>
<div class="doc-status" id="statusBob">on call</div>
</div>
</div>
<div class="txns">
<div class="txn" id="txnA">
<div class="txn-title">Transaction A (Alice)</div>
<div class="txn-log" id="logA"></div>
<button id="btnA" type="button">Alice goes off call</button>
</div>
<div class="txn" id="txnB">
<div class="txn-title">Transaction B (Bob)</div>
<div class="txn-log" id="logB"></div>
<button id="btnB" type="button">Bob goes off call</button>
</div>
</div>
<div class="result-box" id="resultBox"></div>
<div class="btns">
<button id="btnRun" type="button" class="primary">Run both simultaneously</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #1a1a2e; background: #f8f9fc; padding: 12px; }
.scenario { display: flex; flex-direction: column; gap: 10px; }
.rule-box { background: #e8f4fd; border: 1.5px solid #3a86d4; border-radius: 8px; padding: 8px 12px; font-weight: 600; color: #1a5296; font-size: 13px; }
.rule-box.violated { background: #fde8e8; border-color: #d9534f; color: #a02020; }
.rule-box.ok { background: #e8fdf0; border-color: #28a745; color: #155724; }
.db-state { display: flex; gap: 10px; }
.doctor-card { flex: 1; background: #fff; border: 1.5px solid #c8d6e5; border-radius: 10px; padding: 10px 12px; text-align: center; }
.doctor-card.off { opacity: 0.45; }
.doc-name { font-weight: 700; font-size: 15px; margin-bottom: 4px; }
.doc-status { font-size: 12px; background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; display: inline-block; }
.doctor-card.off .doc-status { background: #f5c6cb; color: #721c24; }
.txns { display: flex; gap: 10px; }
.txn { flex: 1; background: #fff; border: 1.5px solid #c8d6e5; border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
.txn-title { font-weight: 700; font-size: 12px; color: #555; text-transform: uppercase; letter-spacing: .04em; }
.txn-log { min-height: 72px; font-size: 11.5px; color: #333; line-height: 1.55; }
.txn-log .step { padding: 1px 0; }
.txn-log .step.read { color: #1a5296; }
.txn-log .step.write { color: #c55a00; }
.txn-log .step.commit { color: #155724; font-weight: 700; }
.txn button { font: 600 12px system-ui; padding: 5px 10px; border-radius: 7px;
border: 1.5px solid #1d3557; background: #1d3557; color: #fff; cursor: pointer; }
.txn button:disabled { opacity: .4; cursor: not-allowed; }
.result-box { min-height: 32px; padding: 8px 12px; border-radius: 8px; font-weight: 700;
font-size: 13px; text-align: center; display: none; }
.result-box.violation { display: block; background: #fde8e8; color: #a02020; border: 1.5px solid #d9534f; }
.result-box.safe { display: block; background: #e8fdf0; color: #155724; border: 1.5px solid #28a745; }
.btns { display: flex; gap: 8px; flex-wrap: wrap; }
button.primary { font: 700 13px system-ui; padding: 7px 16px; border-radius: 8px;
border: 1.5px solid #1d3557; background: #1d3557; color: #fff; cursor: pointer; }
button.ghost { font: 600 13px system-ui; padding: 7px 14px; border-radius: 8px;
border: 1.5px solid #1d3557; background: #fff; color: #1d3557; cursor: pointer; }
button:disabled { opacity: .4; cursor: not-allowed; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Snapshot Isolation write-skew demo.
// Each transaction reads a consistent snapshot at its start time, decides
// it's safe to go off-call (because the snapshot shows 2 doctors on call),
// then writes. Both commit. Final state: 0 doctors on call — the constraint
// "at least 1 on call" is violated. This is write skew: two valid snapshots produce an impossible combined result.
const state = { alice: true, bob: true }; // true = on call
let running = false;
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
function log(id, text, cls) {
const el = document.getElementById(id);
const span = document.createElement('div');
span.className = 'step ' + (cls || '');
span.textContent = text;
el.appendChild(span);
}
function updateCards() {
const ca = document.getElementById('cardAlice');
const cb = document.getElementById('cardBob');
const sa = document.getElementById('statusAlice');
const sb = document.getElementById('statusBob');
ca.className = 'doctor-card' + (state.alice ? '' : ' off');
cb.className = 'doctor-card' + (state.bob ? '' : ' off');
sa.textContent = state.alice ? 'on call' : 'off call';
sb.textContent = state.bob ? 'on call' : 'off call';
}
function reset() {
state.alice = true; state.bob = true; running = false;
document.getElementById('logA').innerHTML = '';
document.getElementById('logB').innerHTML = '';
document.getElementById('resultBox').className = 'result-box';
document.getElementById('resultBox').textContent = '';
document.getElementById('ruleBox').className = 'rule-box';
document.getElementById('btnA').disabled = false;
document.getElementById('btnB').disabled = false;
document.getElementById('btnRun').disabled = false;
updateCards();
console.log('[reset] state:', JSON.stringify(state));
}
async function runTxnA(snapshot) {
const onCall = (snapshot.alice ? 1 : 0) + (snapshot.bob ? 1 : 0);
log('logA', fmt('READ snapshot: alice=%alice%, bob=%bob%', { alice: snapshot.alice, bob: snapshot.bob }), 'read');
log('logA', fmt('Count on call = %onCall% ≥ 1, safe to leave', { onCall: onCall }), 'read');
await delay(600);
log('logA', 'WRITE alice = false', 'write');
state.alice = false;
await delay(300);
log('logA', 'COMMIT ✓', 'commit');
updateCards();
console.log('[txnA] committed. alice=false (snapshot showed', onCall, 'on call)');
}
async function runTxnB(snapshot) {
const onCall = (snapshot.alice ? 1 : 0) + (snapshot.bob ? 1 : 0);
log('logB', fmt('READ snapshot: alice=%alice%, bob=%bob%', { alice: snapshot.alice, bob: snapshot.bob }), 'read');
log('logB', fmt('Count on call = %onCall% ≥ 1, safe to leave', { onCall: onCall }), 'read');
await delay(700);
log('logB', 'WRITE bob = false', 'write');
state.bob = false;
await delay(200);
log('logB', 'COMMIT ✓', 'commit');
updateCards();
console.log('[txnB] committed. bob=false (snapshot showed', onCall, 'on call)');
}
async function runSimultaneous() {
if (running) return;
running = true;
document.getElementById('btnRun').disabled = true;
document.getElementById('btnA').disabled = true;
document.getElementById('btnB').disabled = true;
document.getElementById('logA').innerHTML = '';
document.getElementById('logB').innerHTML = '';
document.getElementById('resultBox').className = 'result-box';
// Both transactions take a snapshot of the CURRENT committed state simultaneously
const snapA = { alice: state.alice, bob: state.bob };
const snapB = { alice: state.alice, bob: state.bob };
console.log('[snapshot] both txns read:', JSON.stringify(snapA));
// Run both transactions concurrently
await Promise.all([runTxnA(snapA), runTxnB(snapB)]);
// Check the constraint
const onCallNow = (state.alice ? 1 : 0) + (state.bob ? 1 : 0);
console.log('[result] doctors on call:', onCallNow, '| state:', JSON.stringify(state));
const rb = document.getElementById('resultBox');
const rule = document.getElementById('ruleBox');
if (onCallNow === 0) {
rb.textContent = 'Write skew! 0 doctors on call — the constraint is violated.';
rb.className = 'result-box violation';
rule.className = 'rule-box violated';
console.log('[write-skew] CONFIRMED: both txns saw 2 on call; both went off; constraint broken.');
} else {
rb.textContent = fmt('Constraint holds: %onCallNow% doctor(s) on call.', { onCallNow: onCallNow });
rb.className = 'result-box safe';
rule.className = 'rule-box ok';
}
}
async function runSingleA() {
if (running) return;
running = true;
document.getElementById('btnA').disabled = true;
document.getElementById('btnRun').disabled = true;
const snap = { alice: state.alice, bob: state.bob };
await runTxnA(snap);
const ok = (state.alice ? 1 : 0) + (state.bob ? 1 : 0) >= 1;
const rb = document.getElementById('resultBox');
rb.textContent = ok ? 'Constraint holds: Alice off call, Bob still on.' : 'Violation!';
rb.className = 'result-box ' + (ok ? 'safe' : 'violation');
running = false;
}
async function runSingleB() {
if (running) return;
running = true;
document.getElementById('btnB').disabled = true;
document.getElementById('btnRun').disabled = true;
const snap = { alice: state.alice, bob: state.bob };
await runTxnB(snap);
const ok = (state.alice ? 1 : 0) + (state.bob ? 1 : 0) >= 1;
const rb = document.getElementById('resultBox');
rb.textContent = ok ? 'Constraint holds: Bob off call, Alice still on.' : 'Violation!';
rb.className = 'result-box ' + (ok ? 'safe' : 'violation');
running = false;
}
document.getElementById('btnRun').addEventListener('click', runSimultaneous);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('btnA').addEventListener('click', runSingleA);
document.getElementById('btnB').addEventListener('click', runSingleB);
// Initial render
updateCards();
console.log('[init] state:', JSON.stringify(state));
Notice: each individual transaction is internally consistent and reads only committed data. The problem is that two valid snapshots, combined through concurrent writes to different rows, produced a state that no serial ordering of the two transactions could ever reach. That is write skew in its purest form.
Comments
Loading comments...