Skip to content

Commit e371553

Browse files
oxzijulianbrost
authored andcommitted
utils.IterateOrderedMap for plugin.FormatMessage
The "Tags" and "Extra Tags" printed in the plugin.FormatMessage were directly read from their map, thus having no order. This resulted in the same NotificationRequest being represented by different messages due to the unordered map. This change was the result of investigating Go's new rangefunc experiment[0]. The utilization of this novel language feature - which can also be indirectly used in the absence of `GOEXPERIMENT=rangefunc` - ensures that the map is traversed in key order. Closes #177. [0]: https://go.dev/wiki/RangefuncExperiment
1 parent 4190d09 commit e371553

File tree

3 files changed

+82
-4
lines changed

3 files changed

+82
-4
lines changed

internal/utils/utils.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package utils
22

33
import (
4+
"cmp"
45
"context"
56
"database/sql"
67
"fmt"
@@ -10,6 +11,7 @@ import (
1011
"github.com/icinga/icingadb/pkg/utils"
1112
"github.com/jmoiron/sqlx"
1213
"github.com/pkg/errors"
14+
"slices"
1315
"strings"
1416
)
1517

@@ -157,3 +159,27 @@ func RemoveNils[T any](slice []*T) []*T {
157159
return ptr == nil
158160
})
159161
}
162+
163+
// IterateOrderedMap implements iter.Seq2 to iterate over a map in the key's order.
164+
//
165+
// This function returns a func yielding key-value-pairs from a given map in the order of their keys, if their type
166+
// is cmp.Ordered.
167+
//
168+
// Please note that currently - being at Go 1.22 - rangefuncs are still an experimental feature and cannot be directly
169+
// used unless compiled with `GOEXPERIMENT=rangefunc`. However, they can still be invoked normally.
170+
// https://go.dev/wiki/RangefuncExperiment
171+
func IterateOrderedMap[K cmp.Ordered, V any](m map[K]V) func(func(K, V) bool) {
172+
keys := make([]K, 0, len(m))
173+
for key := range m {
174+
keys = append(keys, key)
175+
}
176+
slices.Sort(keys)
177+
178+
return func(yield func(K, V) bool) {
179+
for _, key := range keys {
180+
if !yield(key, m[key]) {
181+
return
182+
}
183+
}
184+
}
185+
}

internal/utils/utils_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,52 @@ func TestRemoveNils(t *testing.T) {
2626
})
2727
}
2828
}
29+
30+
func TestIterateOrderedMap(t *testing.T) {
31+
tests := []struct {
32+
name string
33+
in map[int]string
34+
outKeys []int
35+
}{
36+
{"empty", map[int]string{}, nil},
37+
{"single", map[int]string{1: "foo"}, []int{1}},
38+
{"few-numbers", map[int]string{1: "a", 2: "b", 3: "c"}, []int{1, 2, 3}},
39+
{
40+
"1k-numbers",
41+
func() map[int]string {
42+
m := make(map[int]string)
43+
for i := 0; i < 1000; i++ {
44+
m[i] = "foo"
45+
}
46+
return m
47+
}(),
48+
func() []int {
49+
keys := make([]int, 1000)
50+
for i := 0; i < 1000; i++ {
51+
keys[i] = i
52+
}
53+
return keys
54+
}(),
55+
},
56+
}
57+
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
var outKeys []int
61+
62+
// Either run with GOEXPERIMENT=rangefunc or wait for rangefuncs to land in the next Go release.
63+
// for k, _ := range IterateOrderedMap(tt.in) {
64+
// outKeys = append(outKeys, k)
65+
// }
66+
67+
// In the meantime, it can be invoked as follows.
68+
IterateOrderedMap(tt.in)(func(k int, v string) bool {
69+
assert.Equal(t, tt.in[k], v)
70+
outKeys = append(outKeys, k)
71+
return true
72+
})
73+
74+
assert.Equal(t, tt.outKeys, outKeys)
75+
})
76+
}
77+
}

pkg/plugin/plugin.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"github.com/icinga/icinga-notifications/internal/utils"
78
"github.com/icinga/icinga-notifications/pkg/rpc"
89
"github.com/icinga/icingadb/pkg/types"
910
"io"
@@ -201,15 +202,17 @@ func FormatMessage(writer io.Writer, req *NotificationRequest) {
201202
}
202203
_, _ = fmt.Fprintf(writer, "Object: %s\n\n", req.Object.Url)
203204
_, _ = writer.Write([]byte("Tags:\n"))
204-
for k, v := range req.Object.Tags {
205+
utils.IterateOrderedMap(req.Object.Tags)(func(k, v string) bool {
205206
_, _ = fmt.Fprintf(writer, "%s: %s\n", k, v)
206-
}
207+
return true
208+
})
207209

208210
if len(req.Object.ExtraTags) > 0 {
209211
_, _ = writer.Write([]byte("\nExtra Tags:\n"))
210-
for k, v := range req.Object.ExtraTags {
212+
utils.IterateOrderedMap(req.Object.ExtraTags)(func(k, v string) bool {
211213
_, _ = fmt.Fprintf(writer, "%s: %s\n", k, v)
212-
}
214+
return true
215+
})
213216
}
214217

215218
_, _ = fmt.Fprintf(writer, "\nIncident: %s", req.Incident.Url)

0 commit comments

Comments
 (0)