env-08-two-phase-commit
0.000
Challenge · difficulty 5/5
# Two-phase commit
Four nodes are provisioned on a private, no-internet network. Implement the two-phase commit (2PC)
protocol so that two transactions, `t1` then `t2`, are decided **atomically** — every participant
reaches the *same* outcome for a transaction, even though they vote independently.
Nodes and the program each one runs:
- **participant0**, **participant1**, **participant2** — identical participants. Create
**`participant.py`** (launched as `python participant.py`) on each. It listens on `$PORT`. Each
participant's vote is fixed by a file `votes.txt` in its working directory (already present): two
lines, the vote (`yes` or `no`) for `t1` then for `t2`. Endpoints (JSON body `{"txn": "<id>"}`):
- `POST /prepare` — respond `200` with the body `yes` or `no` (this transaction's vote from `votes.txt`).
- `POST /commit` — record this transaction's outcome as `committed`. Respond `200`.
- `POST /abort` — record this transaction's outcome as `aborted`. Respond `200`.
The participant maintains a file `state.txt` listing each decided transaction, one per line as
`<txn>=<committed|aborted>`, sorted by transaction id (e.g. `t1=committed`).
- **coordinator** — create **`coordinator.py`** (launched as `python coordinator.py`). For each
transaction `t1` then `t2`, it runs 2PC against all three participants (reachable at
`PEER_PARTICIPANT0_HOST`/`PEER_PARTICIPANT0_PORT`, …`PARTICIPANT1`…, …`PARTICIPANT2`…):
1. send `POST /prepare` to every participant and collect the votes;
2. if **all** voted `yes`, the decision is `commit`; if **any** voted `no`, it is `abort`;
3. send `POST /commit` (or `POST /abort`) to **every** participant accordingly.
It records the decisions in a file `decision.txt`, one per line as `<txn>=<commit|abort>`, in order
(`t1` then `t2`).
The task is solved when:
- the coordinator's `decision.txt` is `t1=commit` then `t2=abort`;
- **every** participant's `state.txt` is `t1=committed` then `t2=aborted` — i.e. all three commit
`t1` together, and all three abort `t2` together (the single `no` vote forces a global abort).
Only the Python standard library is available (no internet, no pip). Retry connections that are
briefly refused while a peer is starting.
Proposed solution
[turn 1] list_nodes({}) -> {"nodes": [{"name": "participant0", "provides": "python participant.py", "ports": [9000], "needs": []}, {"name": "participant1", "provides": "python participant.py", "ports": [9000], "needs": []}, {"n
[turn 2] write_file({"node": "participant0", "path": "participant.py", "content": "#!/usr/bin/env python3\nimport json\nimport os\nimport socket\nimport threading\nfrom http.server import HTTPServer, BaseHTTPRequestHandl) -> {"ok": true, "node": "participant0", "path": "participant.py"}
[turn 2] write_file({"node": "participant1", "path": "participant.py", "content": "#!/usr/bin/env python3\nimport json\nimport os\nimport socket\nimport threading\nfrom http.server import HTTPServer, BaseHTTPRequestHandl) -> {"ok": true, "node": "participant1", "path": "participant.py"}
[turn 2] write_file({"node": "participant2", "path": "participant.py", "content": "#!/usr/bin/env python3\nimport json\nimport os\nimport socket\nimport threading\nfrom http.server import HTTPServer, BaseHTTPRequestHandl) -> {"ok": true, "node": "participant2", "path": "participant.py"}
[turn 2] write_file({"node": "coordinator", "path": "coordinator.py", "content": "#!/usr/bin/env python3\nimport json\nimport os\nimport socket\nimport time\nfrom urllib.request import Request, urlopen\nfrom urllib.error) -> {"ok": true, "node": "coordinator", "path": "coordinator.py"}
[turn 3] verify({}) -> {"passed": false, "checks": [{"name": "coordinator decided t1=commit, t2=abort", "ok": false, "detail": "{'t1': 'abort', 't2': 'abort'}"}, {"name": "t1 committed on every participant", "ok": false, "d
[turn 4] read_file({"node": "participant0", "path": "votes.txt"}) -> {"content": "yes\nyes\n"}
[turn 4] read_file({"node": "participant1", "path": "votes.txt"}) -> {"content": "yes\nno\n"}
[turn 4] read_file({"node": "participant2", "path": "votes.txt"}) -> {"content": "yes\nyes\n"}
[turn 5] read_file({"node": "coordinator", "path": "decision.txt"}) -> {"content": "t1=abort\nt2=abort\n"}
[turn 5] read_file({"node": "participant0", "path": "state.txt"}) -> {"error": "no such file: state.txt"}
[turn 5] read_file({"node": "participant1", "path": "state.txt"}) -> {"error": "no such file: state.txt"}
[turn 5] read_file({"node": "participant2", "path": "state.txt"}) -> {"error": "no such file: state.txt"}
[turn 6] write_file({"node": "participant0", "path": "participant.py", "content": "#!/usr/bin/env python3\nimport json\nimport os\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\nfrom urllib.parse import urlp) -> {"ok": true, "node": "participant0", "path": "participant.py"}
[turn 6] write_file({"node": "participant1", "path": "participant.py", "content": "#!/usr/bin/env python3\nimport json\nimport os\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\nfrom urllib.parse import urlp) -> {"ok": true, "node": "participant1", "path": "participant.py"}
[turn 6] write_file({"node": "participant2", "path": "participant.py", "content": "#!/usr/bin/env python3\nimport json\nimport os\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\nfrom urllib.parse import urlp) -> {"ok": true, "node": "participant2", "path": "participant.py"}
[turn 7] verify({}) -> {"passed": false, "checks": [{"name": "coordinator decided t1=commit, t2=abort", "ok": false, "detail": "{'t1': 'abort', 't2': 'abort'}"}, {"name": "t1 committed on every participant", "ok": false, "d
[turn 8] read_file({"node": "coordinator", "path": "decision.txt"}) -> {"content": "t1=abort\nt2=abort\n"}
[turn 9] write_file({"node": "coordinator", "path": "coordinator.py", "content": "#!/usr/bin/env python3\nimport json\nimport os\nimport socket\nimport time\nfrom urllib.request import Request, urlopen\nfrom urllib.error) -> {"ok": true, "node": "coordinator", "path": "coordinator.py"}
[turn 10] run({"node": "coordinator", "cmd": "python coordinator.py"}) -> {"rc": 0, "output": "Starting 2PC for transaction t1\nFailed to get vote from participant0\nStarting 2PC for transaction t2\nFailed to get vote from participant0\n2PC completed. Decisions written to d
[turn 11] run({"node": "participant0", "cmd": "python participant.py &"}) -> {"rc": 0, "output": "Traceback (most recent call last):\n File \"/work/participant.py\", line 79, in <module>\n run_server()\n File \"/work/participant.py\", line 75, in run_server\n server =
[turn 12] run({"node": "participant0", "cmd": "echo $PORT"}) -> {"rc": 0, "output": "9000\n"}
[turn 12] run({"node": "participant0", "cmd": "netstat -tlnp | grep :9000"}) -> {"rc": 1, "output": "sh: 1: netstat: not found\n\n"}
[turn 13] run({"node": "participant0", "cmd": "curl -s http://localhost:9000/prepare --data '{\"txn\": \"t1\"}' || echo \"Failed to connect\""}) -> {"rc": 0, "output": "Failed to connect\nsh: 1: curl: not found\n"}
[turn 14] run({"node": "participant0", "cmd": "python3 -c \"import socket; s = socket.socket(); s.connect(('localhost', 9000)); print('Connected'); s.close()\" || echo \"Failed to connect\""}) -> {"rc": 0, "output": "Connected\n"}
[turn 15] run({"node": "coordinator", "cmd": "python3 -c \"import socket; s = socket.socket(); s.connect(('participant0', 9000)); print('Connected'); s.close()\" || echo \"Failed to connect\""}) -> {"rc": 0, "output": "Connected\n"}
[turn 16] run({"node": "coordinator", "cmd": "python3 -c \"\nimport json\nfrom urllib.request import Request, urlopen\nfrom urllib.error import URLError\n\nurl = 'http://participant0:9000/prepare'\ntry:\n req = ) -> {"rc": 0, "output": "Status: 200\nResponse: yes\n"}