Skip to content

Commit b2577fd

Browse files
bailinheCopilot
andauthored
fix: topological sort before restore (#233)
* fix: topological sort before restore The groups DB model has a self-reference (`group->approverGropup`). This PR implements a simple topological sort to ensure that the dependencies are created before itself. Signed-off-by: Bailin He <bahe@equinix.com> * applied review changes Signed-off-by: Bailin He <bahe@equinix.com> * Update internal/backupper/groups.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: Bailin He <bahe@equinix.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 51f8567 commit b2577fd

2 files changed

Lines changed: 152 additions & 2 deletions

File tree

internal/backupper/groups.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package backupper
2+
3+
import (
4+
crdbModels "github.com/metal-toolbox/governor-api/internal/models/crdb"
5+
psqlModels "github.com/metal-toolbox/governor-api/internal/models/psql"
6+
"go.uber.org/zap"
7+
)
8+
9+
type sortable struct {
10+
dbmodel interface{}
11+
id string
12+
parent string
13+
}
14+
15+
func (b *Backupper) crdbGroupsToSortable(in crdbModels.GroupSlice) []*sortable {
16+
out := []*sortable{}
17+
18+
for _, g := range in {
19+
sg := &sortable{
20+
dbmodel: g,
21+
id: g.ID,
22+
}
23+
24+
if g.ApproverGroup.Valid {
25+
sg.parent = g.ApproverGroup.String
26+
}
27+
28+
out = append(out, sg)
29+
}
30+
31+
return out
32+
}
33+
34+
func (b *Backupper) psqlGroupsToSortable(in psqlModels.GroupSlice) []*sortable {
35+
out := []*sortable{}
36+
37+
for _, g := range in {
38+
sg := &sortable{
39+
dbmodel: g,
40+
id: g.ID,
41+
}
42+
43+
if g.ApproverGroup.Valid {
44+
sg.parent = g.ApproverGroup.String
45+
}
46+
47+
out = append(out, sg)
48+
}
49+
50+
return out
51+
}
52+
53+
// sortPSQLGroups sorts PSQL groups topologically and returns the correctly typed slice
54+
func (b *Backupper) sortPSQLGroups(in []*sortable) psqlModels.GroupSlice {
55+
sorted := b.sort(in)
56+
if sorted == nil {
57+
return nil
58+
}
59+
60+
result := make(psqlModels.GroupSlice, 0, len(sorted))
61+
62+
for _, g := range sorted {
63+
if group, ok := g.dbmodel.(*psqlModels.Group); ok {
64+
result = append(result, group)
65+
}
66+
}
67+
68+
return result
69+
}
70+
71+
// sortCRDBGroups sorts CRDB groups topologically and returns the correctly typed slice
72+
func (b *Backupper) sortCRDBGroups(in []*sortable) crdbModels.GroupSlice {
73+
sorted := b.sort(in)
74+
if sorted == nil {
75+
return nil
76+
}
77+
78+
result := make(crdbModels.GroupSlice, 0, len(sorted))
79+
80+
for _, g := range sorted {
81+
if group, ok := g.dbmodel.(*crdbModels.Group); ok {
82+
result = append(result, group)
83+
}
84+
}
85+
86+
return result
87+
}
88+
89+
// sort sorts the groups topologically based on their dependencies.
90+
func (b *Backupper) sort(in []*sortable) []*sortable {
91+
existsMap := make(map[string]*sortable)
92+
for _, g := range in {
93+
existsMap[g.id] = g
94+
}
95+
96+
var (
97+
visited = make(map[string]bool)
98+
recursionMap = make(map[string]bool)
99+
result = []*sortable{}
100+
dfs func(group *sortable) bool
101+
)
102+
103+
// topological sort with DFS
104+
dfs = func(node *sortable) bool {
105+
if recursionMap[node.id] {
106+
b.logger.Warn("Detected cycle in group dependencies", zap.String("group_id", node.id))
107+
return false
108+
}
109+
110+
if visited[node.id] {
111+
return true
112+
}
113+
114+
visited[node.id] = true
115+
recursionMap[node.id] = true
116+
117+
// if node has a non-empty parent and exists in map
118+
if node.parent != "" {
119+
if m, exists := existsMap[node.parent]; exists {
120+
if !dfs(m) {
121+
// circular dependency detected
122+
recursionMap[node.id] = false
123+
return false
124+
}
125+
}
126+
}
127+
128+
recursionMap[node.id] = false
129+
130+
result = append(result, node)
131+
132+
return true
133+
}
134+
135+
for _, node := range in {
136+
if !visited[node.id] {
137+
if !dfs(node) {
138+
return nil
139+
}
140+
}
141+
}
142+
143+
return result
144+
}

internal/backupper/restore.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@ func (b *Backupper) Restore(ctx context.Context, reader io.Reader) error {
2929
return err
3030
}
3131

32+
sortableGroups := b.psqlGroupsToSortable(data.Groups)
33+
sorted := b.sortPSQLGroups(sortableGroups)
34+
3235
restorationGroups = []restorationGroup{
3336
{name: "application types", data: toInsertable(data.ApplicationTypes)},
34-
{name: "groups", data: toInsertable(data.Groups)},
37+
{name: "groups", data: toInsertable(sorted)},
3538
{name: "users", data: toInsertable(data.Users)},
3639
{name: "extensions", data: toInsertable(data.Extensions)},
3740
{name: "notification targets", data: toInsertable(data.NotificationTargets)},
@@ -58,9 +61,12 @@ func (b *Backupper) Restore(ctx context.Context, reader io.Reader) error {
5861
return err
5962
}
6063

64+
sortableGroups := b.crdbGroupsToSortable(data.Groups)
65+
sorted := b.sortCRDBGroups(sortableGroups)
66+
6167
restorationGroups = []restorationGroup{
6268
{name: "application types", data: toInsertable(data.ApplicationTypes)},
63-
{name: "groups", data: toInsertable(data.Groups)},
69+
{name: "groups", data: toInsertable(sorted)},
6470
{name: "users", data: toInsertable(data.Users)},
6571
{name: "extensions", data: toInsertable(data.Extensions)},
6672
{name: "notification targets", data: toInsertable(data.NotificationTargets)},

0 commit comments

Comments
 (0)