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" "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 }