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
4 files VERSION+1 −1src/exchange/exchange.v+15 −3src/exchange/exchange_test.v+35 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.731+3.0.74

view file

src/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 the

view file

src/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 	}

view file

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 }

view file