ЁЯПл The SchoolтА║ЁЯМР Distributed SystemsтА║ЁЯФП рдзрдбрд╛ 10 тАФ Locks, leases & fencing: рд╕рдВрдкрдгрд╛рд░реЗ рдХреБрд▓реВрдк
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯФП рдзрдбрд╛ 10 тАФ Locks, leases & fencing: рд╕рдВрдкрдгрд╛рд░реЗ рдХреБрд▓реВрдк

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 10 ┬╖ рдорд╛рдЧреЗ: lesson-09-raft ┬╖ рдкреБрдвреЗ: lesson-11-exactly-once


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ09, рдЖрдгрд┐ distributed locks: machines рдкрд▓реАрдХрдбрдЪреЗ lock lease рдХрд╛ рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ (рддреЗ рд╕рдВрдкрддреЗ), holder рдерд╛рдВрдмрд▓рд╛ рддрд░ рдлрдХреНрдд lease рд╕реБрд░рдХреНрд╖рд┐рдд рдХрд╛ рдирд╛рд╣реА, рдЖрдгрд┐ рддреНрдпрд╛рд╡рд░рдЪрд╛ рдЙрдкрд╛рдп тАФ storage рддрдкрд╛рд╕рддреЗ рддреЛ fencing token. рд╣реЗ Martin Kleppmann рдпрд╛рдВрдЪреЗ рд╕реБрдкреНрд░рд╕рд┐рджреНрдз рдЙрджрд╛рд╣рд░рдг рдЖрд╣реЗ. dist/demo.py рдордзреАрд▓ locks() рдЖрдгрд┐ dist/sim.py рдордзреАрд▓ LockService + Storage.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдПрдХрд╛ рд╡реЗрд│реА рдлрдХреНрдд рдПрдХрдЪ рд╡реНрдпрдХреНрддреА timetable рдмрджрд▓реВ рд╢рдХрддреЗ. рдореНрд╣рдгреВрди рдореБрдЦреНрдп office рдордзреНрдпреЗ рдПрдХ рдХрд┐рд▓реНрд▓реА ЁЯФС рдЕрд╕рддреЗ.

рдкрдг рдХрддрд░рд┐рдирд╛рдиреЗ рдХрд┐рд▓реНрд▓реА рдШреЗрддрд▓реА рдЖрдгрд┐ рдордЧ рддреА рдЖрдард╡рдбрд╛рднрд░ рдЭреЛрдкреВрди рдЧреЗрд▓реА рддрд░? рдХреЛрдгреАрдЪ timetable рдмрджрд▓реВ рд╢рдХрдгрд╛рд░ рдирд╛рд╣реА. рдореНрд╣рдгреВрди рдХрд┐рд▓реНрд▓реА рдореНрд╣рдгрдЬреЗ рдПрдХ lease: рддреА 1 second рдЪрд╛рд▓рддреЗ, рдордЧ рддреА рддрд┐рдЪреА рдорд╛рдирд▓реА рдЬрд╛рдд рдирд╛рд╣реА. ЁЯХР

рдХрддрд░рд┐рдирд╛ рдХрд┐рд▓реНрд▓реА рдШреЗрддреЗ. рдореБрдЦреНрдп office рддреНрдпрд╛рд╡рд░ рдПрдХ рдХреНрд░рдорд╛рдВрдХ рд▓рд┐рд╣рд┐рддреЗ: token 1. рдордЧ рдХрддрд░рд┐рдирд╛ 2 seconds рд╕рд╛рдареА рдЧреЛрдарддреЗ (рддрд┐рдЪрд╛ computer memory рд╕рд╛рдл рдХрд░рдгреНрдпрд╛рдд рд╡реНрдпрд╕реНрдд рдЖрд╣реЗ тАФ рдПрдХ рд▓рд╛рдВрдм рдерд╛рдВрдмрд╛). рдЖрдкрдг рдЧреЛрдард▓реЛ рд╣реЛрддреЛ рд╣реЗ рддрд┐рд▓рд╛ рдХрд│рдд рдирд╛рд╣реА.

1.5 seconds рд▓рд╛ рддрд┐рдЪреА lease рд╕рдВрдкрд▓реЗрд▓реА рдЕрд╕рддреЗ. рдРрд╢реНрд╡рд░реНрдпрд╛ рдХрд┐рд▓реНрд▓реА рдШреЗрддреЗ: token 2. рддреА timetable v2 рд▓рд┐рд╣рд┐рддреЗ.

2.0 seconds рд▓рд╛ рдХрддрд░рд┐рдирд╛ рдЬрд╛рдЧреА рд╣реЛрддреЗ. рддрд┐рдЪреНрдпрд╛ рдордирд╛рдд рдХрд╛рд╣реАрдЪ рд╡реЗрд│ рдЧреЗрд▓реЗрд▓рд╛ рдирд╛рд╣реА. рддрд┐рд▓рд╛ рд╡рд╛рдЯрддреЗ рдХрд┐рд▓реНрд▓реА рдЕрдЬреВрди рддрд┐рдЪреНрдпрд╛рдХрдбреЗрдЪ рдЖрд╣реЗ, рдЖрдгрд┐ рддреА timetable v1 рд▓рд┐рд╣рд┐рддреЗ. ЁЯШм

Timetable рд▓рд╛ рдХреЛрдг рд╡рд╛рдЪрд╡рддреЗ? рдХрдкрд╛рдЯ ЁЯЧДя╕П рддреНрдпрд╛рдиреЗ рдкрд╛рд╣рд┐рд▓реЗрд▓рд╛ рд╕рд░реНрд╡рд╛рдд рдореЛрдард╛ token рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рддреЗ. рддреНрдпрд╛рдиреЗ 2 рдкрд╛рд╣рд┐рд▓рд╛ рдЖрд╣реЗ. рдХрддрд░рд┐рдирд╛ 1 рджрд╛рдЦрд╡рддреЗ. рдХрдкрд╛рдЯ рдореНрд╣рдгрддреЗ: "рдирд╛рд╣реА. рддреВ рдЦреВрдк рдЬреБрдиреА рдЖрд╣реЗрд╕."

ЁЯЧ║я╕П рдЖрдХреГрддреА

sequenceDiagram
    participant K as Katrina
    participant L as Lock service
    participant A as Aishwarya
    participant S as Storage
    K->>L: acquire at 0 s
    L-->>K: lease 1 s, token 1
    Note over K: long pause, 2 s
    A->>L: acquire at 1.5 s, lease expired
    L-->>A: token 2
    A->>S: write v2 with token 2
    S-->>A: wrote, highest token is 2
    K->>S: write v1 with token 1 at 2.0 s
    S-->>K: refused, already saw 2

ЁЯЧ║я╕П рд░реЗрдЦрд╛рдЯрд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/distributed-systems/lesson-diagrams.html#l10

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг locks рд╕рд╣рд╕рд╛ рдмрд░реЛрдмрд░рдкрдгрд╛рд╕рд╛рдареА рд╡рд╛рдкрд░рд▓реЗ рдЬрд╛рддрд╛рдд: "рдХрдзреАрдЪ рджреЛрди writers рдирдХреЛрдд". рдЬреЗ lock рдлрдХреНрдд рдмрд╣реБрддреЗрдХ рд╡реЗрд│рд╛ exclusive рдЕрд╕рддреЗ рддреЗ рдХрд╛рд░реНрдпрдХреНрд╖рдорддреЗрд╕рд╛рдареА рдареАрдХ рдЖрд╣реЗ (рддреЗрдЪ рдХрд╛рдо рджреЛрдирджрд╛ рдЯрд╛рд│рдгреЗ) рдЖрдгрд┐ рдмрд░реЛрдмрд░рдкрдгрд╛рд╕рд╛рдареА рдзреЛрдХрд╛рджрд╛рдпрдХ рдЖрд╣реЗ (data рдмрд┐рдШрдбрддреЛ). Fencing token "рдмрд╣реБрддреЗрдХ рд╡реЗрд│рд╛" рдЪреЗ "рдиреЗрд╣рдореА" рдХрд░рддреЛ, рдЬреЛрдкрд░реНрдпрдВрдд storage рддреЛ рддрдкрд╛рд╕рддреЗ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

dist/sim.py рдордзреАрд▓ LockService(lease_ms) acquire(who, now) рдордзреНрдпреЗ lock рддреЗрд╡реНрд╣рд╛рдЪ рджреЗрддреЗ рдЬреЗрд╡реНрд╣рд╛ рддреЗ рдХреЛрдгрд╛рдХрдбреЗрдЪ рдирд╕рддреЗ рдХрд┐рдВрд╡рд╛ lease рд╕рдВрдкрд▓реЗрд▓реА рдЕрд╕рддреЗ (now >= expires); рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА lock рджреЗрддрд╛рдирд╛ рддреЗ token рдордзреНрдпреЗ 1 рдорд┐рд│рд╡рддреЗ рдЖрдгрд┐ рддреЛ рдкрд░рдд рдХрд░рддреЗ (рдХрд┐рдВрд╡рд╛ lock рдЖрдзреАрдЪ рдШреЗрддрд▓реЗрд▓реЗ рдЕрд╕реЗрд▓ рддрд░ None). Storage.write(token, value, fencing=True) рддреНрдпрд╛рдиреЗ рдкрд╛рд╣рд┐рд▓реЗрд▓реНрдпрд╛ рд╕рд░реНрд╡рд╛рдд рдореЛрдареНрдпрд╛ token рдкреЗрдХреНрд╖рд╛ рд▓рд╣рд╛рди token рдирд╛рдХрд╛рд░рддреЗ; fencing=False рдЕрд╕реЗрд▓ рддрд░ рддреЗ рдХрд╛рд╣реАрд╣реА рд╕реНрд╡реАрдХрд╛рд░рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 dist/demo.py locks
python3 - <<'EOF'
import sys; sys.path.insert(0, "dist"); from sim import LockService, Storage
for lease in (1000, 3000):
    ls, st = LockService(lease_ms=lease), Storage()
    t1 = ls.acquire("katrina", 0)
    t2 = ls.acquire("aishwarya", 1500)
    print(f"lease {lease} ms: Katrina token {t1}, Aishwarya at 1.5 s тЖТ {t2}")
ls, st = LockService(1000), Storage()
t1 = ls.acquire("katrina", 0); t2 = ls.acquire("aishwarya", 1500); t3 = ls.acquire("dipika", 2600)
print("tokens:", t1, t2, t3)
for tok, who in ((t3, "dipika"), (t2, "aishwarya"), (t1, "katrina")):
    print(f"  {who} writes with token {tok} тЖТ {st.write(tok, who + ' timetable')}")
print("  final value:", st.value)
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

locks рд╣реЗ print рдХрд░рддреЗ:

тФАтФА Katrina takes the timetable lock, lease 1 s, fencing token 1 тАФ then pauses for 2 s (a long GC)
   at 1.5 s the lease has expired; Aishwarya takes the lock тЖТ token 2 and writes тЖТ wrote 'timetable v2'
   Katrina wakes at 2.0 s, still thinks she holds the lock, writes with token 1 тЖТ refused token 1 (already saw 2)
   without fencing tokens тЖТ wrote 'timetable v1' тАФ newer work silently overwritten
   a lease alone is not enough; the storage must check the token

рддреБрдордЪрд╛ snippet рд╣реЗ print рдХрд░рддреЛ:

lease 1000 ms: Katrina token 1, Aishwarya at 1.5 s тЖТ 2
lease 3000 ms: Katrina token 1, Aishwarya at 1.5 s тЖТ None
tokens: 1 2 3
  dipika writes with token 3 тЖТ wrote 'dipika timetable'
  aishwarya writes with token 2 тЖТ refused token 2 (already saw 3)
  katrina writes with token 1 тЖТ refused token 1 (already saw 3)
  final value: dipika timetable

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рд▓рд╛рдВрдм lease (3 s) рдЕрдбрдЪрдг рдлрдХреНрдд рдкреБрдвреЗ рдврдХрд▓рддреЗ: рдРрд╢реНрд╡рд░реНрдпрд╛рд▓рд╛ рдерд╛рдВрдмрд╛рд╡реЗ рд▓рд╛рдЧрддреЗ, рдЖрдгрд┐ 4-second рдЪрд╛ pause рддреА рдкреБрдиреНрд╣рд╛ рдореЛрдбреЗрд▓. рдкреНрд░рддреНрдпреЗрдХ pause рд╕рд╛рдареА рдкреБрд░реЗрд╢реА рд▓рд╛рдВрдм рдЕрд╢реА рдХреЛрдгрддреАрд╣реА lease рдирд╛рд╣реА. рддреАрди holders рд╕рд░реНрд╡рд╛рдд рд╡рд╛рдИрдЯ рдХреНрд░рдорд╛рдиреЗ рдЙрд╢рд┐рд░рд╛ рдЖрд▓реЗ рддрд░реА storage рдиреЗ рдлрдХреНрдд рд╕рд░реНрд╡рд╛рдд рдирд╡реНрдпрд╛ holder рдЪреЗрдЪ рдХрд╛рдо рдареЗрд╡рд▓реЗ тАФ token рдард░рд╡рддреЛ, рдкреЛрд╣реЛрдЪрдгреНрдпрд╛рдЪрд╛ рдХреНрд░рдо рдирд╡реНрд╣реЗ рдЖрдгрд┐ рдХреЛрдгрд╛рдЪреЗ рдШрдбреНрдпрд╛рд│рд╣реА рдирд╡реНрд╣реЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

Redis locks тАФ single-instance pattern рдореНрд╣рдгрдЬреЗ NX (рдлрдХреНрдд рдирд╕реЗрд▓ рддрд░) рдЖрдгрд┐ expiry рд╕рд╣ SET. On a real account:

redis-cli SET lock:timetable 9f2c41d7 NX PX 30000   # OK = you hold it for 30 s; (nil) = someone else does

Value рдЕрдЬреВрди рддреБрдордЪреАрдЪ рдЕрд╕реЗрд▓ рддрд░рдЪ рддреЗ рд╕реЛрдбрд╛ (рдПрдХ рдЫреЛрдЯреА Lua script рддреБрд▓рдирд╛ рдХрд░рддреЗ, рдордЧ delete рдХрд░рддреЗ). Redlock рд╣реЗрдЪ рдЕрдиреЗрдХ рд╕реНрд╡рддрдВрддреНрд░ Redis servers рд╡рд░ рдкреБрдиреНрд╣рд╛ рдХрд░рддреЗ рдЖрдгрд┐ рдмрд╣реБрдордд рдореЛрдЬрддреЗ. Kleppmann рдпрд╛рдВрдЪреА рдЯреАрдХрд╛: Redlock рдЪреА рд╕реБрд░рдХреНрд╖рд┐рддрддрд╛ timing рдЧреГрд╣реАрддрдХрд╛рдВрд╡рд░ (рдорд░реНрдпрд╛рджрд┐рдд pauses, рдорд░реНрдпрд╛рджрд┐рдд clock drift) рдЕрд╡рд▓рдВрдмреВрди рдЖрд╣реЗ, рдЖрдгрд┐ рддреЗ рдХреЛрдгрддрд╛рд╣реА fencing token рддрдпрд╛рд░ рдХрд░рдд рдирд╛рд╣реА. Redis locks рдХрд╛рд░реНрдпрдХреНрд╖рдорддреЗрд╕рд╛рдареА рд╡рд╛рдкрд░рд╛ (duplicate рдХрд╛рдо рдЯрд╛рд│рдгреЗ); рдмрд░реЛрдмрд░рдкрдгрд╛рд╕рд╛рдареА, token рджреЗрдгрд╛рд░реЗ lock рдЖрдгрд┐ рддреЛ рддрдкрд╛рд╕рдгрд╛рд░реЗ storage рд╡рд╛рдкрд░рд╛.

etcd тАФ lease рдЖрдгрд┐ lock; рдкреНрд░рддреНрдпреЗрдХ key рд▓рд╛ рдПрдХ revision рдХреНрд░рдорд╛рдВрдХ рдЕрд╕рддреЛ рдЬреЛ рд╕рдВрдкреВрд░реНрдг store рдордзреНрдпреЗ рдлрдХреНрдд рд╡рд╛рдврдд рдЬрд╛рддреЛ, рддреНрдпрд╛рдореБрд│реЗ lock key рдЪреА revision fencing token рдореНрд╣рдгреВрди рд╡рд╛рдкрд░рддрд╛ рдпреЗрддреЗ:

etcdctl lease grant 10                       # lease 694d7b1c2f0e4a1b granted with TTL(10s)
etcdctl lock timetable ./edit-timetable.sh      # runs the command while holding the lock

ZooKeeper тАФ рдПрдХ ephemeral sequential node рддрдпрд╛рд░ рдХрд░рд╛; рд╕рд░реНрд╡рд╛рдд рд▓рд╣рд╛рди рдХреНрд░рдорд╛рдВрдХрд╛рдХрдбреЗ lock рдЕрд╕рддреЗ, рдЖрдгрд┐ holder рдЪреЗ session рд╕рдВрдкрд▓реНрдпрд╛рд╡рд░ рддреА node рдирд╛рд╣реАрд╢реА рд╣реЛрддреЗ. рддрд┐рдЪрд╛ sequence number (рдХрд┐рдВрд╡рд╛ transaction ID zxid) fencing token рдореНрд╣рдгреВрди рдХрд╛рдо рдХрд░рддреЛ:

zkCli.sh create -e -s /locks/timetable- katrina   # Created /locks/timetable-0000000007

Kubernetes controllers Lease object рд╡рд╛рдкрд░реВрди leader рдирд┐рд╡рдбрддрд╛рдд (coordination.k8s.io): holderIdentity, leaseDurationSeconds, renewTime.

kubectl get lease -n kube-system                # kube-controller-manager, kube-scheduler тАж

Fencing рдЪреА storage рдмрд╛рдЬреВ тАФ рдЙрджрд╛рд╣рд░рдгрд╛рд░реНрде SQL рдордзреНрдпреЗ, write рдлрдХреНрдд рдирд╡реНрдпрд╛ token рд╕рд╣ рд╕реНрд╡реАрдХрд╛рд░рд╛:

UPDATE timetable SET body = $1, fence = $2
WHERE id = 'term-2' AND fence < $2;          -- 0 rows updated тЖТ a stale holder; stop

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ lock рд╕рд╛рдареА рджреЛрди рдкреНрд░рд╢реНрди рд╡рд┐рдЪрд╛рд░рд╛: "holder 30 seconds рдерд╛рдВрдмрд▓рд╛ рддрд░ рдХрд╛рдп рд╣реЛрдИрд▓?" рдЖрдгрд┐ "token рдХреБрдареЗ рддрдкрд╛рд╕рд▓рд╛ рдЬрд╛рддреЛ?". рджреБрд╕рд▒реНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рдЪреЗ рдЙрддреНрддрд░ "рдХреБрдареЗрдЪ рдирд╛рд╣реА" рдЕрд╕реЗрд▓, рддрд░ рддреЗ lock рдлрдХреНрдд рдХрд╛рд░реНрдпрдХреНрд╖рдорддреЗрдЪреЗ рд╕рдВрд░рдХреНрд╖рдг рдХрд░рддреЗ, рдмрд░реЛрдмрд░рдкрдгрд╛рдЪреЗ рдирд╡реНрд╣реЗ.

тПня╕П рдкреБрдвреЗ

Locks рджреЛрди writers рдирд╛ рдерд╛рдВрдмрд╡рддрд╛рдд. Retries рдПрдХ рд╡реЗрдЧрд│реАрдЪ рдЕрдбрдЪрдг рдирд┐рд░реНрдорд╛рдг рдХрд░рддрд╛рдд: рддреЛрдЪ message рджреЛрдирджрд╛ рд╣рд╛рддрд╛рд│рд▓рд╛ рдЬрд╛рдгреЗ. рдкреБрдвреЗ: exactly-once рдЪреЗ рдорд┐рдердХ.

git checkout lesson-11-exactly-once

ЁЯФП Lesson 10 тАФ Locks, leases & fencing: a lock that expires

ЁЯУН You are here: Lesson 10 of 12 ┬╖ Previous: lesson-09-raft ┬╖ Next: lesson-11-exactly-once


ЁЯУж What's in this branch

Lessons 01тАУ09, plus distributed locks: why a lock across machines must be a lease (it expires), why a lease alone is not safe when the holder pauses, and the fix тАФ a fencing token that the storage checks. This is Martin Kleppmann's well-known example. locks() in dist/demo.py and LockService + Storage in dist/sim.py.

ЁЯзТ Explain like I'm 5

Only one person may edit the timetable at a time. So there is a key ЁЯФС at the head office.

But what if Katrina takes the key and then falls asleep for a week? Nobody could edit the timetable. So the key is a lease: it works for 1 second, then it stops counting as hers. ЁЯХР

Katrina takes the key. The head office writes a number on it: token 1. Then Katrina freezes for 2 seconds (her computer is busy cleaning memory тАФ a long pause). She does not know she froze.

At 1.5 seconds her lease has run out. Aishwarya takes the key: token 2. She writes timetable v2.

At 2.0 seconds Katrina wakes up. In her mind, no time has passed. She thinks she still holds the key, and writes timetable v1. ЁЯШм

What saves the timetable? The filing cabinet ЁЯЧДя╕П remembers the biggest token it has seen. It has seen 2. Katrina shows 1. The cabinet says: "No. You are too old."

ЁЯЧ║я╕П Diagram

sequenceDiagram
    participant K as Katrina
    participant L as Lock service
    participant A as Aishwarya
    participant S as Storage
    K->>L: acquire at 0 s
    L-->>K: lease 1 s, token 1
    Note over K: long pause, 2 s
    A->>L: acquire at 1.5 s, lease expired
    L-->>A: token 2
    A->>S: write v2 with token 2
    S-->>A: wrote, highest token is 2
    K->>S: write v1 with token 1 at 2.0 s
    S-->>K: refused, already saw 2

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/distributed-systems/lesson-diagrams.html#l10

тЭУ What

ЁЯдФ Why

Because locks are usually used for correctness: "never two writers". A lock that is only usually exclusive is fine for efficiency (avoid doing the same work twice) and dangerous for correctness (corrupt data). A fencing token turns "usually" into "always", as long as the storage checks it.

ЁЯФз How (in this repo)

LockService(lease_ms) in dist/sim.py grants the lock in acquire(who, now) only if nobody holds it or the lease has expired (now >= expires); each grant adds 1 to token and returns it (or None when the lock is taken). Storage.write(token, value, fencing=True) refuses a token smaller than the highest it has seen; with fencing=False it accepts anything.

ЁЯзк Try it

python3 dist/demo.py locks
python3 - <<'EOF'
import sys; sys.path.insert(0, "dist"); from sim import LockService, Storage
for lease in (1000, 3000):
    ls, st = LockService(lease_ms=lease), Storage()
    t1 = ls.acquire("katrina", 0)
    t2 = ls.acquire("aishwarya", 1500)
    print(f"lease {lease} ms: Katrina token {t1}, Aishwarya at 1.5 s тЖТ {t2}")
ls, st = LockService(1000), Storage()
t1 = ls.acquire("katrina", 0); t2 = ls.acquire("aishwarya", 1500); t3 = ls.acquire("dipika", 2600)
print("tokens:", t1, t2, t3)
for tok, who in ((t3, "dipika"), (t2, "aishwarya"), (t1, "katrina")):
    print(f"  {who} writes with token {tok} тЖТ {st.write(tok, who + ' timetable')}")
print("  final value:", st.value)
EOF

тЬЕ Verify тАФ what you should see

locks prints:

тФАтФА Katrina takes the timetable lock, lease 1 s, fencing token 1 тАФ then pauses for 2 s (a long GC)
   at 1.5 s the lease has expired; Aishwarya takes the lock тЖТ token 2 and writes тЖТ wrote 'timetable v2'
   Katrina wakes at 2.0 s, still thinks she holds the lock, writes with token 1 тЖТ refused token 1 (already saw 2)
   without fencing tokens тЖТ wrote 'timetable v1' тАФ newer work silently overwritten
   a lease alone is not enough; the storage must check the token

Your snippet prints:

lease 1000 ms: Katrina token 1, Aishwarya at 1.5 s тЖТ 2
lease 3000 ms: Katrina token 1, Aishwarya at 1.5 s тЖТ None
tokens: 1 2 3
  dipika writes with token 3 тЖТ wrote 'dipika timetable'
  aishwarya writes with token 2 тЖТ refused token 2 (already saw 3)
  katrina writes with token 1 тЖТ refused token 1 (already saw 3)
  final value: dipika timetable

ЁЯПБ What you just proved

A longer lease (3 s) only moves the problem: Aishwarya must wait, and a 4-second pause would break it again. There is no lease long enough for every pause. With three holders arriving late in the worst order, the storage still kept only the newest holder's work тАФ the token decides, not the arrival order and not anyone's clock.

тЪая╕П Common mistakes

ЁЯПн In production

Redis locks тАФ the single-instance pattern is SET with NX (only if not present) and an expiry. On a real account:

redis-cli SET lock:timetable 9f2c41d7 NX PX 30000   # OK = you hold it for 30 s; (nil) = someone else does

Release it only if the value is still yours (a small Lua script compares, then deletes). Redlock repeats this on several independent Redis servers and counts a majority. Kleppmann's critique: Redlock's safety depends on timing assumptions (bounded pauses, bounded clock drift), and it produces no fencing token. Use Redis locks for efficiency (avoid duplicate work); for correctness, use a lock that gives a token and a storage that checks it.

etcd тАФ a lease plus a lock; every key has a revision number that only goes up across the whole store, so the lock key's revision can be the fencing token:

etcdctl lease grant 10                       # lease 694d7b1c2f0e4a1b granted with TTL(10s)
etcdctl lock timetable ./edit-timetable.sh      # runs the command while holding the lock

ZooKeeper тАФ create an ephemeral sequential node; the lowest number holds the lock, and the node disappears when the holder's session ends. Its sequence number (or the transaction ID zxid) works as a fencing token:

zkCli.sh create -e -s /locks/timetable- katrina   # Created /locks/timetable-0000000007

Kubernetes controllers elect a leader with a Lease object (coordination.k8s.io): holderIdentity, leaseDurationSeconds, renewTime.

kubectl get lease -n kube-system                # kube-controller-manager, kube-scheduler тАж

The storage side of fencing тАФ for example in SQL, accept a write only with a newer token:

UPDATE timetable SET body = $1, fence = $2
WHERE id = 'term-2' AND fence < $2;          -- 0 rows updated тЖТ a stale holder; stop

ЁЯПн Why this matters in production: for every lock, ask two questions: "what happens if the holder pauses for 30 seconds?" and "where is the token checked?". If the answer to the second is "nowhere", the lock only protects efficiency, not correctness.

тПня╕П Next

Locks stop two writers. Retries create a different problem: the same message done twice. Next: the exactly-once myth.

git checkout lesson-11-exactly-once
тЖР PreviousraftNext тЖТexactly once

This page is the lesson's README from the lesson-10-locks-leases branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.