summaryrefslogtreecommitdiffstats
path: root/pkg/utils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:40:59 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:40:59 +0000
commitbc4e624732bd51c0dd1e9529cf228e8c23127732 (patch)
treed95dab8960e9d02d3b95f8653074ad2e54ca207c /pkg/utils
parentInitial commit. (diff)
downloadicingadb-bc4e624732bd51c0dd1e9529cf228e8c23127732.tar.xz
icingadb-bc4e624732bd51c0dd1e9529cf228e8c23127732.zip
Adding upstream version 1.1.1.upstream/1.1.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'pkg/utils')
-rw-r--r--pkg/utils/utils.go231
-rw-r--r--pkg/utils/utils_test.go54
2 files changed, 285 insertions, 0 deletions
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
new file mode 100644
index 0000000..4b0fe1d
--- /dev/null
+++ b/pkg/utils/utils.go
@@ -0,0 +1,231 @@
+package utils
+
+import (
+ "context"
+ "crypto/sha1"
+ "fmt"
+ "github.com/go-sql-driver/mysql"
+ "github.com/icinga/icingadb/pkg/contracts"
+ "github.com/lib/pq"
+ "github.com/pkg/errors"
+ "golang.org/x/exp/utf8string"
+ "math"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+ "unicode"
+)
+
+// FromUnixMilli creates and returns a time.Time value
+// from the given milliseconds since the Unix epoch ms.
+func FromUnixMilli(ms int64) time.Time {
+ sec, dec := math.Modf(float64(ms) / 1e3)
+
+ return time.Unix(int64(sec), int64(dec*(1e9)))
+}
+
+// Name returns the declared name of type t.
+// Name is used in combination with Key
+// to automatically guess an entity's
+// database table and Redis key.
+func Name(t interface{}) string {
+ s := strings.TrimLeft(fmt.Sprintf("%T", t), "*")
+
+ return s[strings.LastIndex(s, ".")+1:]
+}
+
+// TableName returns the table of t.
+func TableName(t interface{}) string {
+ if tn, ok := t.(contracts.TableNamer); ok {
+ return tn.TableName()
+ } else {
+ return Key(Name(t), '_')
+ }
+}
+
+// Key returns the name with all Unicode letters mapped to lower case letters,
+// with an additional separator in front of each original upper case letter.
+func Key(name string, sep byte) string {
+ return ConvertCamelCase(name, unicode.LowerCase, sep)
+}
+
+// Timed calls the given callback with the time that has elapsed since the start.
+//
+// Timed should be installed by defer:
+//
+// func TimedExample(logger *zap.SugaredLogger) {
+// defer utils.Timed(time.Now(), func(elapsed time.Duration) {
+// logger.Debugf("Executed job in %s", elapsed)
+// })
+// job()
+// }
+func Timed(start time.Time, callback func(elapsed time.Duration)) {
+ callback(time.Since(start))
+}
+
+// BatchSliceOfStrings groups the given keys into chunks of size count and streams them into a returned channel.
+func BatchSliceOfStrings(ctx context.Context, keys []string, count int) <-chan []string {
+ batches := make(chan []string)
+
+ go func() {
+ defer close(batches)
+
+ for i := 0; i < len(keys); i += count {
+ end := i + count
+ if end > len(keys) {
+ end = len(keys)
+ }
+
+ select {
+ case batches <- keys[i:end]:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+
+ return batches
+}
+
+// IsContextCanceled returns whether the given error is context.Canceled.
+func IsContextCanceled(err error) bool {
+ return errors.Is(err, context.Canceled)
+}
+
+// Checksum returns the SHA-1 checksum of the data.
+func Checksum(data interface{}) []byte {
+ var chksm [sha1.Size]byte
+
+ switch data := data.(type) {
+ case string:
+ chksm = sha1.Sum([]byte(data))
+ case []byte:
+ chksm = sha1.Sum(data)
+ default:
+ panic(fmt.Sprintf("Unable to create checksum for type %T", data))
+ }
+
+ return chksm[:]
+}
+
+// Fatal panics with the given error.
+func Fatal(err error) {
+ panic(err)
+}
+
+// IsDeadlock returns whether the given error signals serialization failure.
+func IsDeadlock(err error) bool {
+ var e *mysql.MySQLError
+ if errors.As(err, &e) {
+ switch e.Number {
+ case 1205, 1213:
+ return true
+ default:
+ return false
+ }
+ }
+
+ var pe *pq.Error
+ if errors.As(err, &pe) {
+ switch pe.Code {
+ case "40001", "40P01":
+ return true
+ }
+ }
+
+ return false
+}
+
+var ellipsis = utf8string.NewString("...")
+
+// Ellipsize shortens s to <=limit runes and indicates shortening by "...".
+func Ellipsize(s string, limit int) string {
+ utf8 := utf8string.NewString(s)
+ switch {
+ case utf8.RuneCount() <= limit:
+ return s
+ case utf8.RuneCount() <= ellipsis.RuneCount():
+ return ellipsis.String()
+ default:
+ return utf8.Slice(0, limit-ellipsis.RuneCount()) + ellipsis.String()
+ }
+}
+
+// ConvertCamelCase converts a (lower) CamelCase string into various cases.
+// _case must be unicode.Lower or unicode.Upper.
+//
+// Example usage:
+//
+// # snake_case
+// ConvertCamelCase(s, unicode.Lower, '_')
+//
+// # SCREAMING_SNAKE_CASE
+// ConvertCamelCase(s, unicode.Upper, '_')
+//
+// # kebab-case
+// ConvertCamelCase(s, unicode.Lower, '-')
+//
+// # SCREAMING-KEBAB-CASE
+// ConvertCamelCase(s, unicode.Upper, '-')
+//
+// # other.separator
+// ConvertCamelCase(s, unicode.Lower, '.')
+func ConvertCamelCase(s string, _case int, sep byte) string {
+ r := []rune(s)
+ b := strings.Builder{}
+ b.Grow(len(r) + 2) // nominal 2 bytes of extra space for inserted delimiters
+
+ b.WriteRune(unicode.To(_case, r[0]))
+ for _, r := range r[1:] {
+ if sep != 0 && unicode.IsUpper(r) {
+ b.WriteByte(sep)
+ }
+
+ b.WriteRune(unicode.To(_case, r))
+ }
+
+ return b.String()
+}
+
+// AppName returns the name of the executable that started this program (process).
+func AppName() string {
+ exe, err := os.Executable()
+ if err != nil {
+ exe = os.Args[0]
+ }
+
+ return filepath.Base(exe)
+}
+
+// MaxInt returns the larger of the given integers.
+func MaxInt(x, y int) int {
+ if x > y {
+ return x
+ }
+
+ return y
+}
+
+// JoinHostPort is like its equivalent in net., but handles UNIX sockets as well.
+func JoinHostPort(host string, port int) string {
+ if strings.HasPrefix(host, "/") {
+ return host
+ }
+
+ return net.JoinHostPort(host, fmt.Sprint(port))
+}
+
+// ChanFromSlice takes a slice of values and returns a channel from which these values can be received.
+// This channel is closed after the last value was sent.
+func ChanFromSlice[T any](values []T) <-chan T {
+ ch := make(chan T, len(values))
+ for _, value := range values {
+ ch <- value
+ }
+
+ close(ch)
+
+ return ch
+}
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
new file mode 100644
index 0000000..b0ea54b
--- /dev/null
+++ b/pkg/utils/utils_test.go
@@ -0,0 +1,54 @@
+package utils
+
+import (
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestChanFromSlice(t *testing.T) {
+ t.Run("Nil", func(t *testing.T) {
+ ch := ChanFromSlice[int](nil)
+ require.NotNil(t, ch)
+ requireClosedEmpty(t, ch)
+ })
+
+ t.Run("Empty", func(t *testing.T) {
+ ch := ChanFromSlice([]int{})
+ require.NotNil(t, ch)
+ requireClosedEmpty(t, ch)
+ })
+
+ t.Run("NonEmpty", func(t *testing.T) {
+ ch := ChanFromSlice([]int{42, 23, 1337})
+ require.NotNil(t, ch)
+ requireReceive(t, ch, 42)
+ requireReceive(t, ch, 23)
+ requireReceive(t, ch, 1337)
+ requireClosedEmpty(t, ch)
+ })
+}
+
+// requireReceive is a helper function to check if a value can immediately be received from a channel.
+func requireReceive(t *testing.T, ch <-chan int, expected int) {
+ t.Helper()
+
+ select {
+ case v, ok := <-ch:
+ require.True(t, ok, "receiving should return a value")
+ require.Equal(t, expected, v)
+ default:
+ require.Fail(t, "receiving should not block")
+ }
+}
+
+// requireReceive is a helper function to check if the channel is closed and empty.
+func requireClosedEmpty(t *testing.T, ch <-chan int) {
+ t.Helper()
+
+ select {
+ case _, ok := <-ch:
+ require.False(t, ok, "receiving from channel should not return anything")
+ default:
+ require.Fail(t, "receiving should not block")
+ }
+}