Skip to content

Commit

Permalink
Bug fix: fixing ref conflict case that can cause tags to be temporari…
Browse files Browse the repository at this point in the history
…ly removed on a replica
  • Loading branch information
fulghum committed Jan 24, 2025
1 parent f19ceef commit 95032b2
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 5 deletions.
16 changes: 14 additions & 2 deletions go/libraries/doltcore/sqle/read_replica_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"

Expand Down Expand Up @@ -509,9 +510,20 @@ func getReplicationRefs(ctx *sql.Context, rrd ReadReplicaDatabase) (
func refsToDelete(remRefs, localRefs []doltdb.RefWithHash) []doltdb.RefWithHash {
toDelete := make([]doltdb.RefWithHash, 0, len(localRefs))
var i, j int

// Before we map remote refs to local refs to determine which refs to delete, we need to sort them
// by Ref.String() – this ensures a unique identifier that does not conflict with other refs, unlike
// Ref.GetPath(), which can conflict if a branch or tag has the same name.
sort.Slice(remRefs, func(i, j int) bool {
return remRefs[i].Ref.String() < remRefs[j].Ref.String()
})
sort.Slice(localRefs, func(i, j int) bool {
return localRefs[i].Ref.String() < localRefs[j].Ref.String()
})

for i < len(remRefs) && j < len(localRefs) {
rem := remRefs[i].Ref.GetPath()
local := localRefs[j].Ref.GetPath()
rem := remRefs[i].Ref.String()
local := localRefs[j].Ref.String()
if rem == local {
i++
j++
Expand Down
9 changes: 7 additions & 2 deletions go/libraries/doltcore/sqle/replication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,17 @@ func TestReplicationBranches(t *testing.T) {
local: []string{"feature4", "feature5", "feature6", "feature7", "feature8", "feature9"},
expToDelete: []string{"feature4", "feature5", "feature6", "feature7", "feature8", "feature9"},
},
{
remote: []string{"main", "new1", "a1"},
local: []string{"main", "a1"},
expToDelete: []string{},
},
}

for _, tt := range tests {
remoteRefs := make([]doltdb.RefWithHash, len(tt.remote))
for i := range tt.remote {
remoteRefs[i] = doltdb.RefWithHash{Ref: ref.NewRemoteRef("", tt.remote[i])}
remoteRefs[i] = doltdb.RefWithHash{Ref: ref.NewBranchRef(tt.remote[i])}
}
localRefs := make([]doltdb.RefWithHash, len(tt.local))
for i := range tt.local {
Expand All @@ -96,6 +101,6 @@ func TestReplicationBranches(t *testing.T) {
for i := range diff {
diffNames[i] = diff[i].Ref.GetPath()
}
assert.Equal(t, diffNames, tt.expToDelete)
assert.Equal(t, tt.expToDelete, diffNames)
}
}
43 changes: 42 additions & 1 deletion integration-tests/bats/replication.bats
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,48 @@ teardown() {
[[ "$output" =~ "b1" ]] || false
}

# When a replica pulls refs, the remote refs are compared with the local refs to identify which local refs
# need to be deleted. Branches, tags, and remotes all share the ref space and previous versions of Dolt could
# incorrectly map remote refs and local refs, resulting in local refs being incorrectly removed, until future
# runs of replica synchronization.
@test "replication: local tag refs are not deleted" {
# Configure repo1 to push changes on commit and create tag a1
cd repo1
dolt config --local --add sqlserver.global.dolt_replicate_to_remote remote1
dolt sql -q "call dolt_tag('a1');"

# Configure repo2 to pull changes on read
cd ..
dolt clone file://./rem1 repo2
cd repo2
dolt config --local --add sqlserver.global.dolt_read_replica_remote origin
dolt config --local --add sqlserver.global.dolt_replicate_all_heads 1
run dolt sql -q "select tag_name from dolt_tags;"
[ "$status" -eq 0 ]
[[ "$output" =~ "| tag_name |" ]] || false
[[ "$output" =~ "| a1 |" ]] || false

# Create branch new1 in repo1 – "new1" sorts after "main", but before "a1", and previous
# versions of Dolt had problems computing which local refs to delete in this case.
cd ../repo1
dolt sql -q "call dolt_branch('new1');"

# Confirm that tag a1 has not been deleted. Note that we need to check for this immediately after
# creating branch new1 (i.e. before looking at branches), because the bug in the previous versions
# of Dolt would only manifest in the next command, and would be fixed by later remote pulls.
cd ../repo2
run dolt sql -q "select tag_name from dolt_tags;"
[ "$status" -eq 0 ]
[[ "$output" =~ "| tag_name |" ]] || false
[[ "$output" =~ "| a1 |" ]] || false

# Try again to make sure the results are stable
run dolt sql -q "select tag_name from dolt_tags;"
[ "$status" -eq 0 ]
[[ "$output" =~ "| tag_name |" ]] || false
[[ "$output" =~ "| a1 |" ]] || false
}

@test "replication: pull branch delete current branch" {
skip "broken by latest transaction changes"

Expand Down Expand Up @@ -627,7 +669,6 @@ SQL
}

@test "replication: pull all heads pulls tags" {

dolt clone file://./rem1 repo2
cd repo2
dolt checkout -b new_feature
Expand Down

0 comments on commit 95032b2

Please sign in to comment.