<p class="hint">Two transactions read/write accounts <b>A</b> and <b>B</b>. Click an
operation to append it to the schedule. Conflicting operations on the same account
(read-write, write-read, write-write) add an arrow to the conflict graph. The
schedule is <b>serializable iff the graph has no cycle</b>.</p>
<div class="ops">
<div class="col t1">
<div class="lbl">T1</div>
<button data-tx="1" data-op="R" data-acc="A" type="button">R1(A)</button>
<button data-tx="1" data-op="W" data-acc="A" type="button">W1(A)</button>
<button data-tx="1" data-op="R" data-acc="B" type="button">R1(B)</button>
<button data-tx="1" data-op="W" data-acc="B" type="button">W1(B)</button>
</div>
<div class="col t2">
<div class="lbl">T2</div>
<button data-tx="2" data-op="R" data-acc="A" type="button">R2(A)</button>
<button data-tx="2" data-op="W" data-acc="A" type="button">W2(A)</button>
<button data-tx="2" data-op="R" data-acc="B" type="button">R2(B)</button>
<button data-tx="2" data-op="W" data-acc="B" type="button">W2(B)</button>
</div>
</div>
<div class="sched-wrap">
<div class="sched-title">Schedule</div>
<div id="sched" class="sched"></div>
</div>
<div class="graph">
<div class="node n1">T1</div>
<div id="arrows" class="arrows">no conflicts yet</div>
<div class="node n2">T2</div>
</div>
<div class="status" id="status">Add operations to build a schedule.</div>
<div class="btns">
<button id="lost" type="button">Lost-update interleave</button>
<button id="serial" type="button">Serial T1 then T2</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.ops { display: flex; gap: 1rem; margin: .3rem 0 .6rem; }
.col { display: flex; flex-direction: column; gap: .35rem; flex: 1; }
.lbl { font-weight: 700; font-size: .85rem; color: #1d3557; }
.col.t2 .lbl { color: #b5651d; }
.ops button { font: 600 13px ui-monospace, monospace; padding: .4rem; border-radius: 7px; cursor: pointer; border: 1px solid #1d3557; background: #eef3f8; color: #1d3557; }
.col.t2 button { border-color: #b5651d; background: #fbf2e8; color: #8a4d12; }
.sched-wrap { margin: .3rem 0; }
.sched-title { font-size: .8rem; font-weight: 600; color: #555; margin-bottom: .2rem; }
.sched { display: flex; flex-wrap: wrap; gap: .3rem; min-height: 2rem; padding: .4rem; background: #f4f6f9; border-radius: 8px; }
.tok { font: 700 13px ui-monospace, monospace; padding: .2rem .45rem; border-radius: 6px; }
.tok.tx1 { background: #d7e3f0; color: #1d3557; }
.tok.tx2 { background: #f3e2cf; color: #8a4d12; }
.graph { display: flex; align-items: center; justify-content: center; gap: .8rem; margin: .7rem 0; }
.node { width: 46px; height: 46px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font: 700 15px system-ui; }
.n1 { background: #1d3557; color: #fff; }
.n2 { background: #b5651d; color: #fff; }
.arrows { font: 600 13px ui-monospace, monospace; color: #444; text-align: center; min-width: 120px; }
.status { font-size: 1rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
.btns button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
.btns button.ghost { background: #fff; color: #1d3557; }
// Conflict-serializability test. A schedule is a list of operations, each
// {tx, op:'R'|'W', acc:'A'|'B'}. Two operations CONFLICT when they belong to
// different transactions, touch the same account, and at least one is a write.
// For each conflicting pair we add a directed edge earlier.tx -> later.tx in the
// conflict (precedence) graph. The schedule is conflict-serializable IFF this
// graph is ACYCLIC. With two transactions, a cycle = both edges 1->2 and 2->1.
const schedule = [];
function conflict(a, b){
return a.tx !== b.tx && a.acc === b.acc && (a.op === 'W' || b.op === 'W');
}
// Returns {edges:Set('1->2'...), cycle:bool}
function analyze(sched){
const edges = new Set();
for (let i = 0; i < sched.length; i++)
for (let j = i + 1; j < sched.length; j++)
if (conflict(sched[i], sched[j])) edges.add(sched[i].tx + '->' + sched[j].tx);
// Generic cycle detection (DFS) over txs present.
const adj = {};
edges.forEach(e => { const [u, v] = e.split('->'); (adj[u] = adj[u] || []).push(v); });
const WHITE = 0, GRAY = 1, BLACK = 2, color = {};
Object.keys(adj).forEach(n => color[n] = WHITE);
(sched.length ? [...new Set(sched.map(s => '' + s.tx))] : []).forEach(n => { if (!(n in color)) color[n] = WHITE; });
let cyc = false;
function dfs(u){
color[u] = GRAY;
(adj[u] || []).forEach(v => {
if (color[v] === GRAY) cyc = true;
else if (color[v] === WHITE) dfs(v);
});
color[u] = BLACK;
}
Object.keys(color).forEach(n => { if (color[n] === WHITE) dfs(n); });
return { edges, cycle: cyc };
}
function add(tx, op, acc){ schedule.push({ tx, op, acc }); render(); }
function render(){
const s = document.getElementById('sched'); s.innerHTML = '';
schedule.forEach(o => {
const d = document.createElement('span');
d.className = 'tok tx' + o.tx;
d.textContent = o.op + o.tx + '(' + o.acc + ')';
s.appendChild(d);
});
const { edges, cycle } = analyze(schedule);
const a = document.getElementById('arrows');
a.textContent = edges.size ? [...edges].join(' ') : 'no conflicts yet';
const st = document.getElementById('status');
if (!schedule.length){ st.textContent = 'Add operations to build a schedule.'; st.className = 'status'; }
else if (cycle){ st.textContent = '✗ Cycle in conflict graph — NOT serializable.'; st.className = 'status bad'; }
else { st.textContent = '✓ Acyclic conflict graph — serializable.'; st.className = 'status ok'; }
}
document.querySelectorAll('.ops button').forEach(b =>
b.addEventListener('click', () => add(+b.dataset.tx, b.dataset.op, b.dataset.acc)));
document.getElementById('lost').addEventListener('click', () => {
schedule.length = 0;
// Classic lost update: R1(A) R2(A) W1(A) W2(A) -> edges 1->2 and 2->1 => cycle.
[['R',1,'A'],['R',2,'A'],['W',1,'A'],['W',2,'A']].forEach(([op,tx,acc]) => schedule.push({tx,op,acc}));
render(); console.log('lost-update edges:', [...analyze(schedule).edges]);
});
document.getElementById('serial').addEventListener('click', () => {
schedule.length = 0;
// T1 fully, then T2 fully: only 1->2 edges, no cycle.
[['R',1,'A'],['W',1,'A'],['R',1,'B'],['W',1,'B'],['R',2,'A'],['W',2,'A']].forEach(([op,tx,acc]) => schedule.push({tx,op,acc}));
render();
});
document.getElementById('reset').addEventListener('click', () => { schedule.length = 0; render(); });
render();
<p class="hint">Two transactions read/write accounts <b>A</b> and <b>B</b>. Click an
operation to append it to the schedule. Conflicting operations on the same account
(read-write, write-read, write-write) add an arrow to the conflict graph. The
schedule is <b>serializable iff the graph has no cycle</b>.</p>
<div class="ops">
<div class="col t1">
<div class="lbl">T1</div>
<button data-tx="1" data-op="R" data-acc="A" type="button">R1(A)</button>
<button data-tx="1" data-op="W" data-acc="A" type="button">W1(A)</button>
<button data-tx="1" data-op="R" data-acc="B" type="button">R1(B)</button>
<button data-tx="1" data-op="W" data-acc="B" type="button">W1(B)</button>
</div>
<div class="col t2">
<div class="lbl">T2</div>
<button data-tx="2" data-op="R" data-acc="A" type="button">R2(A)</button>
<button data-tx="2" data-op="W" data-acc="A" type="button">W2(A)</button>
<button data-tx="2" data-op="R" data-acc="B" type="button">R2(B)</button>
<button data-tx="2" data-op="W" data-acc="B" type="button">W2(B)</button>
</div>
</div>
<div class="sched-wrap">
<div class="sched-title">Schedule</div>
<div id="sched" class="sched"></div>
</div>
<div class="graph">
<div class="node n1">T1</div>
<div id="arrows" class="arrows">no conflicts yet</div>
<div class="node n2">T2</div>
</div>
<div class="status" id="status">Add operations to build a schedule.</div>
<div class="btns">
<button id="lost" type="button">Lost-update interleave</button>
<button id="serial" type="button">Serial T1 then T2</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.ops { display: flex; gap: 1rem; margin: .3rem 0 .6rem; }
.col { display: flex; flex-direction: column; gap: .35rem; flex: 1; }
.lbl { font-weight: 700; font-size: .85rem; color: #1d3557; }
.col.t2 .lbl { color: #b5651d; }
.ops button { font: 600 13px ui-monospace, monospace; padding: .4rem; border-radius: 7px; cursor: pointer; border: 1px solid #1d3557; background: #eef3f8; color: #1d3557; }
.col.t2 button { border-color: #b5651d; background: #fbf2e8; color: #8a4d12; }
.sched-wrap { margin: .3rem 0; }
.sched-title { font-size: .8rem; font-weight: 600; color: #555; margin-bottom: .2rem; }
.sched { display: flex; flex-wrap: wrap; gap: .3rem; min-height: 2rem; padding: .4rem; background: #f4f6f9; border-radius: 8px; }
.tok { font: 700 13px ui-monospace, monospace; padding: .2rem .45rem; border-radius: 6px; }
.tok.tx1 { background: #d7e3f0; color: #1d3557; }
.tok.tx2 { background: #f3e2cf; color: #8a4d12; }
.graph { display: flex; align-items: center; justify-content: center; gap: .8rem; margin: .7rem 0; }
.node { width: 46px; height: 46px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font: 700 15px system-ui; }
.n1 { background: #1d3557; color: #fff; }
.n2 { background: #b5651d; color: #fff; }
.arrows { font: 600 13px ui-monospace, monospace; color: #444; text-align: center; min-width: 120px; }
.status { font-size: 1rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
.btns button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
.btns button.ghost { background: #fff; color: #1d3557; }
// Conflict-serializability test. A schedule is a list of operations, each
// {tx, op:'R'|'W', acc:'A'|'B'}. Two operations CONFLICT when they belong to
// different transactions, touch the same account, and at least one is a write.
// For each conflicting pair we add a directed edge earlier.tx -> later.tx in the
// conflict (precedence) graph. The schedule is conflict-serializable IFF this
// graph is ACYCLIC. With two transactions, a cycle = both edges 1->2 and 2->1.
const schedule = [];
function conflict(a, b){
return a.tx !== b.tx && a.acc === b.acc && (a.op === 'W' || b.op === 'W');
}
// Returns {edges:Set('1->2'...), cycle:bool}
function analyze(sched){
const edges = new Set();
for (let i = 0; i < sched.length; i++)
for (let j = i + 1; j < sched.length; j++)
if (conflict(sched[i], sched[j])) edges.add(sched[i].tx + '->' + sched[j].tx);
// Generic cycle detection (DFS) over txs present.
const adj = {};
edges.forEach(e => { const [u, v] = e.split('->'); (adj[u] = adj[u] || []).push(v); });
const WHITE = 0, GRAY = 1, BLACK = 2, color = {};
Object.keys(adj).forEach(n => color[n] = WHITE);
(sched.length ? [...new Set(sched.map(s => '' + s.tx))] : []).forEach(n => { if (!(n in color)) color[n] = WHITE; });
let cyc = false;
function dfs(u){
color[u] = GRAY;
(adj[u] || []).forEach(v => {
if (color[v] === GRAY) cyc = true;
else if (color[v] === WHITE) dfs(v);
});
color[u] = BLACK;
}
Object.keys(color).forEach(n => { if (color[n] === WHITE) dfs(n); });
return { edges, cycle: cyc };
}
function add(tx, op, acc){ schedule.push({ tx, op, acc }); render(); }
function render(){
const s = document.getElementById('sched'); s.innerHTML = '';
schedule.forEach(o => {
const d = document.createElement('span');
d.className = 'tok tx' + o.tx;
d.textContent = o.op + o.tx + '(' + o.acc + ')';
s.appendChild(d);
});
const { edges, cycle } = analyze(schedule);
const a = document.getElementById('arrows');
a.textContent = edges.size ? [...edges].join(' ') : 'no conflicts yet';
const st = document.getElementById('status');
if (!schedule.length){ st.textContent = 'Add operations to build a schedule.'; st.className = 'status'; }
else if (cycle){ st.textContent = '✗ Cycle in conflict graph — NOT serializable.'; st.className = 'status bad'; }
else { st.textContent = '✓ Acyclic conflict graph — serializable.'; st.className = 'status ok'; }
}
document.querySelectorAll('.ops button').forEach(b =>
b.addEventListener('click', () => add(+b.dataset.tx, b.dataset.op, b.dataset.acc)));
document.getElementById('lost').addEventListener('click', () => {
schedule.length = 0;
// Classic lost update: R1(A) R2(A) W1(A) W2(A) -> edges 1->2 and 2->1 => cycle.
[['R',1,'A'],['R',2,'A'],['W',1,'A'],['W',2,'A']].forEach(([op,tx,acc]) => schedule.push({tx,op,acc}));
render(); console.log('lost-update edges:', [...analyze(schedule).edges]);
});
document.getElementById('serial').addEventListener('click', () => {
schedule.length = 0;
// T1 fully, then T2 fully: only 1->2 edges, no cycle.
[['R',1,'A'],['W',1,'A'],['R',1,'B'],['W',1,'B'],['R',2,'A'],['W',2,'A']].forEach(([op,tx,acc]) => schedule.push({tx,op,acc}));
render();
});
document.getElementById('reset').addEventListener('click', () => { schedule.length = 0; render(); });
render();
Comments
Loading comments...