546abb43431b
close the format write race by reading back the value the winner wrote, and check the format before the name clash so a remote this koh cannot read says so (3.0.74)
- time
- 2026-09-27 09:56 UTC · 2 weeks ago
- machine
- Junes-MacBook-Pro.local
- agent
- deepseek/deepseek-flash
- session
- koh 3 phases 6-8
- parent
- fc918ffae554
↓ koh-546abb43.tar.gzatlas at this save
files changed4 files
VERSION+1 −1src/exchange/exchange.v+15 −3src/exchange/exchange_test.v+35 −0v.mod+1 −1src/exchange/exchange.v+15 −3
@@ -30,6 +30,16 @@3030 // key existed has no `format` key and counts as this version.3131 const format_value = 'koh-remote 1'3232 33+// put_format records the layout on a repo that has none. put_if_absent never34+// overwrites, so a machine that wrote one while this one read wins the race:35+// its value is read back and checked, never assumed.36+fn put_format(mut backend remote.Backend, repo string) ! {37+ if backend.put_if_absent(format_key(repo), format_value.bytes())! {38+ return39+ }40+ check_format(mut backend, repo)!41+}42+3343 // check_format reads <repo>/format and refuses a remote written by a koh that3444 // speaks another layout, so two versions never half-understand one bucket. An3545 // empty answer means the key is missing: version 1, which `offer` records.@@ -67,6 +77,9 @@6777 return remembered6878 }6979 name := os.base(store.root)80+ // Before the clash check below: a remote this koh cannot read is a "update81+ // koh" answer, not a "rename the folder" one.82+ check_format(mut backend, name)!7083 existing := backend.get(head_key(name))!7184 if existing.found {7285 theirs := existing.body.bytestr().trim_space()@@ -88,9 +101,8 @@88101 head: local.id89102 }90103 if check_format(mut backend, name)! == '' {91- // Offered before this key existed: record the layout now, without92- // overwriting one another machine wrote first.93- backend.put_if_absent(format_key(name), format_value.bytes())!104+ // Offered before this key existed: record the layout now.105+ put_format(mut backend, name)!94106 }95107 remote_head := backend.get(head_key(name))!96108 // Records offered by a machine that has not applied a strip would put thesrc/exchange/exchange_test.v+35 −0
@@ -592,6 +592,10 @@592592 fail_read int593593 // fail every delete while set594594 fail_delete bool595+ // race_format stands in for another machine writing the format key first:596+ // the first `format` write loses, to a value this koh does not speak.597+ race_format bool598+ raced bool595599 }596600 597601 fn (mut f FaultyBackend) get(key string) !remote.Object {@@ -611,6 +615,11 @@611615 if f.fail_write > 0 && f.writes >= f.fail_write {612616 return error('injected write failure')613617 }618+ if f.race_format && !f.raced && key.ends_with('/format') {619+ f.raced = true620+ f.inner.put_if_absent(key, 'koh-remote 2'.bytes())!621+ return false622+ }614623 return f.inner.put_if_absent(key, body)!615624 }616625 @@ -1313,6 +1322,32 @@13131322 }13141323 if _ := fetch_repo(mut backend, 'proj') {13151324 assert false, 'a clone must refuse a format it does not speak'1325+ } else {1326+ assert err.msg().contains('koh-remote 2')1327+ }1328+}1329+1330+// Two machines offering a new repo at once both find no format key; the loser1331+// of the conditional write must read back the winner's value and refuse it,1332+// not carry on against a remote it cannot read.1333+fn test_a_lost_format_write_is_read_back_and_refused() {1334+ base := scratch('format_race')!1335+ defer {1336+ os.rmdir_all(base) or {}1337+ }1338+ mut inner := remote.new_folder(os.join_path(base, 'remote'))!1339+ mut a := open_repo(machine('format_race_a')!)!1340+ defer {1341+ a.close()1342+ }1343+ put(a.root, 'a.txt', 'one\n')!1344+ make_save(a, 'first')!1345+ mut backend := remote.Backend(FaultyBackend{1346+ inner: inner1347+ race_format: true1348+ })1349+ if _ := offer_now(a, mut backend) {1350+ assert false, 'offer must refuse the format another machine wrote first'13161351 } else {13171352 assert err.msg().contains('koh-remote 2')13181353 }v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 name: 'koh3'3- version: '3.0.73'3+ version: '3.0.74'44 description: 'One SQLite file per repo, one line of history.'55 license: 'MIT'66 }