• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

llamerada-jp / colonio / 24928251196

25 Apr 2026 09:54AM UTC coverage: 64.66% (-0.1%) from 64.788%
24928251196

push

github

llamerada-jp
fix datarace on ci

Signed-off-by: Yuji Ito <llamerada.jp@gmail.com>

2675 of 4137 relevant lines covered (64.66%)

43.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.57
/node/internal/network/routing/routing_2d.go
1
/*
2
 * Copyright 2017- Yuji Ito <llamerada.jp@gmail.com>
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package routing
17

18
import (
19
        "fmt"
20
        "log/slog"
21
        "math"
22
        "sync"
23

24
        "github.com/fogleman/delaunay"
25
        proto "github.com/llamerada-jp/colonio/api/colonio/v1alpha"
26
        "github.com/llamerada-jp/colonio/node/internal/geometry"
27
        "github.com/llamerada-jp/colonio/types"
28
        "golang.org/x/exp/maps"
29
)
30

31
type routing2DConfig struct {
32
        logger      *slog.Logger
33
        localNodeID *types.NodeID
34
        geometry    geometry.CoordinateSystem
35
}
36

37
type routeInfo2D struct {
38
        position    *geometry.Coordinate
39
        rootNodeIDs map[types.NodeID]struct{}
40
}
41

42
type routing2D struct {
43
        config          *routing2DConfig
44
        mtx             sync.RWMutex
45
        localPosition   geometry.Coordinate
46
        neighborNodeIDs map[types.NodeID]struct{}
47
        routeInfos      map[types.NodeID]*routeInfo2D
48
}
49

50
func newRouting2D(config *routing2DConfig) *routing2D {
5✔
51
        return &routing2D{
5✔
52
                config:          config,
5✔
53
                neighborNodeIDs: make(map[types.NodeID]struct{}),
5✔
54
                routeInfos:      make(map[types.NodeID]*routeInfo2D),
5✔
55
        }
5✔
56
}
5✔
57

58
func (r *routing2D) updateNodeConnections(connections map[types.NodeID]struct{}) int {
3✔
59
        r.mtx.Lock()
3✔
60
        defer r.mtx.Unlock()
3✔
61

3✔
62
        updated := false
3✔
63

3✔
64
        for nodeID, info := range r.routeInfos {
6✔
65
                for rootNodeIDs := range info.rootNodeIDs {
8✔
66
                        if _, ok := connections[rootNodeIDs]; !ok {
9✔
67
                                delete(info.rootNodeIDs, rootNodeIDs)
4✔
68
                        }
4✔
69
                        if len(info.rootNodeIDs) == 0 {
7✔
70
                                delete(r.routeInfos, nodeID)
2✔
71
                                updated = true
2✔
72
                        }
2✔
73
                }
74
        }
75

76
        if updated && r.neighborNodeIDChanged() {
4✔
77
                return requireExchangeRouting | requireUpdateConnections
1✔
78
        }
1✔
79
        return 0
2✔
80
}
81

82
func (r *routing2D) getNextStep(dst *geometry.Coordinate) *types.NodeID {
4✔
83
        candidateNodeID := &types.NodeLocal
4✔
84
        var candidateInfo *routeInfo2D
4✔
85

4✔
86
        r.mtx.RLock()
4✔
87
        defer r.mtx.RUnlock()
4✔
88

4✔
89
        min := r.config.geometry.GetDistance(&r.localPosition, dst)
4✔
90

4✔
91
        for nodeID, info := range r.routeInfos {
10✔
92
                nodeID := nodeID
6✔
93
                distance := r.config.geometry.GetDistance(info.position, dst)
6✔
94
                if distance < min {
9✔
95
                        min = distance
3✔
96
                        candidateNodeID = &nodeID
3✔
97
                        candidateInfo = info
3✔
98
                }
3✔
99
        }
100

101
        // return immediately if the candidate is the local node
102
        if candidateInfo == nil {
6✔
103
                return candidateNodeID
2✔
104
        }
2✔
105

106
        // return immediately if the candidate is connected directly
107
        if _, ok := candidateInfo.rootNodeIDs[*candidateNodeID]; ok {
3✔
108
                return candidateNodeID
1✔
109
        }
1✔
110

111
        // search the nearest node connected to the candidate from candidate info
112
        min = math.MaxFloat64
1✔
113
        for nodeID := range candidateInfo.rootNodeIDs {
3✔
114
                nodeID := nodeID
2✔
115
                info := r.routeInfos[nodeID]
2✔
116
                distance := r.config.geometry.GetDistance(info.position, dst)
2✔
117
                if distance < min {
3✔
118
                        min = distance
1✔
119
                        candidateNodeID = &nodeID
1✔
120
                }
1✔
121
        }
122
        return candidateNodeID
1✔
123
}
124

125
func (r *routing2D) getNextNodePositions() map[types.NodeID]*geometry.Coordinate {
×
126
        r.mtx.RLock()
×
127
        defer r.mtx.RUnlock()
×
128

×
129
        positions := make(map[types.NodeID]*geometry.Coordinate)
×
130
        for nodeID, info := range r.routeInfos {
×
131
                if info.position == nil {
×
132
                        continue
×
133
                }
134
                if _, ok := info.rootNodeIDs[nodeID]; !ok {
×
135
                        continue
×
136
                }
137
                positions[nodeID] = info.position
×
138
        }
139

140
        return positions
×
141
}
142

143
func (r *routing2D) updateLocalPosition(pos *geometry.Coordinate) int {
4✔
144
        r.mtx.Lock()
4✔
145
        defer r.mtx.Unlock()
4✔
146

4✔
147
        if r.localPosition == *pos {
5✔
148
                return 0
1✔
149
        }
1✔
150

151
        r.localPosition = *pos
3✔
152
        r.neighborNodeIDChanged()
3✔
153

3✔
154
        return requireExchangeRouting
3✔
155
}
156

157
func (r *routing2D) recvRoutingPacket(src *types.NodeID, content *proto.Routing) (int, error) {
3✔
158
        r.mtx.Lock()
3✔
159
        defer r.mtx.Unlock()
3✔
160

3✔
161
        updated := false
3✔
162
        contentPosition := geometry.NewCoordinateFromProto(content.GetR2DPosition())
3✔
163

3✔
164
        if info, ok := r.routeInfos[*src]; !ok {
6✔
165
                updated = true
3✔
166
                r.routeInfos[*src] = &routeInfo2D{
3✔
167
                        position: contentPosition,
3✔
168
                        rootNodeIDs: map[types.NodeID]struct{}{
3✔
169
                                *src: {},
3✔
170
                        },
3✔
171
                }
3✔
172
        } else if _, ok := info.rootNodeIDs[*src]; !ok || !info.position.Equal(contentPosition) {
3✔
173
                updated = true
×
174
                info.position = geometry.NewCoordinateFromProto(content.GetR2DPosition())
×
175
                info.rootNodeIDs[*src] = struct{}{}
×
176
        }
×
177

178
        for nodeIDStr, record := range content.GetNodeRecords() {
7✔
179
                nodeID, err := types.NewNodeIDFromString(nodeIDStr)
4✔
180
                if err != nil {
4✔
181
                        return 0, fmt.Errorf("failed to parse node id %w", err)
×
182
                }
×
183
                if nodeID.Equal(r.config.localNodeID) {
4✔
184
                        continue
×
185
                }
186
                if info, ok := r.routeInfos[*nodeID]; !ok {
5✔
187
                        if record.GetR2DPosition() == nil {
1✔
188
                                continue
×
189
                        }
190
                        nodePosition := geometry.NewCoordinateFromProto(record.GetR2DPosition())
1✔
191
                        updated = true
1✔
192
                        r.routeInfos[*nodeID] = &routeInfo2D{
1✔
193
                                position: nodePosition,
1✔
194
                                rootNodeIDs: map[types.NodeID]struct{}{
1✔
195
                                        *src: {},
1✔
196
                                },
1✔
197
                        }
1✔
198
                } else {
3✔
199
                        if _, ok := info.rootNodeIDs[*src]; !ok {
3✔
200
                                updated = true
×
201
                                info.rootNodeIDs[*src] = struct{}{}
×
202
                        }
×
203
                        if record.GetR2DPosition() == nil {
3✔
204
                                continue
×
205
                        }
206
                        nodePosition := geometry.NewCoordinateFromProto(record.GetR2DPosition())
3✔
207
                        // update position if the node is not connected directly
3✔
208
                        if _, ok := info.rootNodeIDs[*nodeID]; !ok && !info.position.Equal(nodePosition) {
3✔
209
                                updated = true
×
210
                                info.position = nodePosition
×
211
                        }
×
212
                }
213
        }
214

215
        for nodeID, info := range r.routeInfos {
11✔
216
                if nodeID.Equal(src) {
11✔
217
                        continue
3✔
218
                }
219
                if _, ok := info.rootNodeIDs[*src]; !ok {
5✔
220
                        continue
×
221
                }
222
                if _, ok := content.GetNodeRecords()[nodeID.String()]; !ok {
6✔
223
                        updated = true
1✔
224
                        delete(info.rootNodeIDs, *src)
1✔
225
                        if len(info.rootNodeIDs) == 0 {
2✔
226
                                delete(r.routeInfos, nodeID)
1✔
227
                        }
1✔
228
                }
229
        }
230

231
        if updated && r.neighborNodeIDChanged() {
5✔
232
                return requireExchangeRouting | requireUpdateConnections, nil
2✔
233
        }
2✔
234
        return 0, nil
1✔
235
}
236

237
func (r *routing2D) setupRoutingPacket(content *proto.Routing) {
1✔
238
        r.mtx.RLock()
1✔
239
        defer r.mtx.RUnlock()
1✔
240

1✔
241
        content.R2DPosition = r.localPosition.Proto()
1✔
242

1✔
243
        for nodeID, info := range r.routeInfos {
4✔
244
                // skip if the node is not connected directly
3✔
245
                if _, ok := info.rootNodeIDs[nodeID]; !ok {
4✔
246
                        continue
1✔
247
                }
248
                nodeIDStr := nodeID.String()
2✔
249
                if nodeRecord, ok := content.NodeRecords[nodeIDStr]; !ok {
3✔
250
                        content.NodeRecords[nodeIDStr] = &proto.RoutingNodeRecord{
1✔
251
                                R2DPosition: info.position.Proto(),
1✔
252
                        }
1✔
253
                } else {
2✔
254
                        nodeRecord.R2DPosition = info.position.Proto()
1✔
255
                }
1✔
256
        }
257
}
258

259
func (r *routing2D) getConnections() map[types.NodeID]struct{} {
×
260
        r.mtx.RLock()
×
261
        defer r.mtx.RUnlock()
×
262

×
263
        return r.neighborNodeIDs
×
264
}
×
265

266
// private
267
func (r *routing2D) neighborNodeIDChanged() bool {
9✔
268
        neighborNodeIDs := make(map[types.NodeID]struct{})
9✔
269

9✔
270
        if len(r.routeInfos) < 3 {
16✔
271
                for nodeID := range r.routeInfos {
12✔
272
                        neighborNodeIDs[nodeID] = struct{}{}
5✔
273
                }
5✔
274

275
        } else {
2✔
276
                points := make(map[delaunay.Point][]*types.NodeID)
2✔
277
                points[delaunay.Point{
2✔
278
                        X: r.localPosition.X,
2✔
279
                        Y: r.localPosition.Y,
2✔
280
                }] = []*types.NodeID{r.config.localNodeID}
2✔
281
                for nodeID, info := range r.routeInfos {
104✔
282
                        point := delaunay.Point{
102✔
283
                                X: info.position.X,
102✔
284
                                Y: info.position.Y,
102✔
285
                        }
102✔
286
                        points[point] = append(points[point], &nodeID)
102✔
287
                }
102✔
288

289
                // shift the node position if the same position is found
290
                precision := r.config.geometry.GetPrecision()
2✔
291
                for point, nodeIDs := range points {
106✔
292
                        if len(nodeIDs) == 1 {
208✔
293
                                continue
104✔
294
                        }
295
                        delete(points, point)
×
296
                        for _, nodeID := range nodeIDs {
×
297
                                id0, id1 := nodeID.Raw()
×
298
                                newPosition := delaunay.Point{
×
299
                                        X: point.X + precision*float64(id0)/math.MaxUint64,
×
300
                                        Y: point.Y + precision*float64(id1)/math.MaxUint64,
×
301
                                }
×
302
                                points[newPosition] = []*types.NodeID{nodeID}
×
303
                        }
×
304
                }
305

306
                pointSlice := make([]delaunay.Point, 0, len(points))
2✔
307
                for point := range points {
106✔
308
                        pointSlice = append(pointSlice, point)
104✔
309
                }
104✔
310
                triangulation, err := delaunay.Triangulate(pointSlice)
2✔
311
                if err != nil {
2✔
312
                        r.config.logger.Error("failed to triangulate", slog.String("err", err.Error()))
×
313
                        return false
×
314
                }
×
315

316
                for i, h := range triangulation.Halfedges {
572✔
317
                        // h == -1 indicates a boundary edge on the convex hull, which has no opposite half-edge.
570✔
318
                        // For non-boundary edges, skip if already processed (h <= i) to avoid duplicates.
570✔
319
                        if h != -1 && h <= i {
848✔
320
                                continue
278✔
321
                        }
322
                        p1 := &pointSlice[triangulation.Triangles[i]]
292✔
323
                        j := i + 1
292✔
324
                        if i%3 == 2 {
350✔
325
                                j = i - 2
58✔
326
                        }
58✔
327
                        p2 := &pointSlice[triangulation.Triangles[j]]
292✔
328

292✔
329
                        n1 := p2n(points, p1)
292✔
330
                        n2 := p2n(points, p2)
292✔
331

292✔
332
                        if n1 == nil || n2 == nil {
292✔
333
                                continue
×
334
                        }
335
                        if *n1 == *r.config.localNodeID {
296✔
336
                                neighborNodeIDs[*n2] = struct{}{}
4✔
337
                        }
4✔
338
                        if *n2 == *r.config.localNodeID {
295✔
339
                                neighborNodeIDs[*n1] = struct{}{}
3✔
340
                        }
3✔
341
                }
342
        }
343

344
        if !maps.Equal(neighborNodeIDs, r.neighborNodeIDs) {
13✔
345
                r.neighborNodeIDs = neighborNodeIDs
4✔
346
                return true
4✔
347
        }
4✔
348

349
        return false
5✔
350
}
351

352
func p2n(points map[delaunay.Point][]*types.NodeID, p *delaunay.Point) *types.NodeID {
584✔
353
        if nodeIDs, ok := points[*p]; ok {
1,168✔
354
                return nodeIDs[0]
584✔
355
        }
584✔
356
        return nil
×
357
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc