diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b2059d6..62a004632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ # Change Log +## 2025-06-23 - Runtime 0.18.0-alpha.13 + +- fix: time zone retrieval and logging [#906](https://github.com/hypermodeinc/modus/pull/906) + ## 2025-06-23 - Runtime 0.18.0-alpha.12 - fix: ensure valid UTF8 byte sequence in HTTP response [#904](https://github.com/hypermodeinc/modus/pull/904) diff --git a/runtime/hostfunctions/system.go b/runtime/hostfunctions/system.go index b62f81ccf..bf0390b4f 100644 --- a/runtime/hostfunctions/system.go +++ b/runtime/hostfunctions/system.go @@ -55,14 +55,19 @@ func LogMessage(ctx context.Context, level, message string) { func GetTimeInZone(ctx context.Context, tz *string) *string { now := time.Now() - var loc *time.Location + var zoneId string if tz != nil && *tz != "" { - loc = timezones.GetLocation(*tz) + zoneId = *tz } else if tz, ok := ctx.Value(utils.TimeZoneContextKey).(string); ok { - loc = timezones.GetLocation(tz) + zoneId = tz + } else { + logger.Error(ctx).Msg("Time zone not specified.") + return nil } - if loc == nil { + loc, err := timezones.GetLocation(ctx, zoneId) + if err != nil { + logger.Err(ctx, err).Str("tz", zoneId).Msg("Failed to get time zone location.") return nil } @@ -70,10 +75,19 @@ func GetTimeInZone(ctx context.Context, tz *string) *string { return &s } -func GetTimeZoneData(tz, format *string) []byte { +func GetTimeZoneData(ctx context.Context, tz, format *string) []byte { if tz == nil { + logger.Error(ctx).Msg("Time zone not specified.") return nil } - - return timezones.GetTimeZoneData(*tz, *format) + if format == nil { + logger.Error(ctx).Msg("Time zone format not specified.") + return nil + } + data, err := timezones.GetTimeZoneData(ctx, *tz, *format) + if err != nil { + logger.Error(ctx).Err(err).Str("tz", *tz).Msg("Failed to get time zone data.") + return nil + } + return data } diff --git a/runtime/timezones/timezones.go b/runtime/timezones/timezones.go index 11367e104..d10bb1f8e 100644 --- a/runtime/timezones/timezones.go +++ b/runtime/timezones/timezones.go @@ -10,19 +10,18 @@ package timezones import ( + "context" + "errors" + "fmt" "os" "time" "github.com/puzpuzpuz/xsync/v4" ) -type tzInfo struct { - location *time.Location - data []byte -} - var systemTimeZone string -var tzCache = *xsync.NewMap[string, *tzInfo]() +var tzDataCache = *xsync.NewMap[string, []byte]() +var tzLocationCache = *xsync.NewMap[string, *time.Location]() func init() { if tz, err := getSystemLocalTimeZone(); err == nil { @@ -42,48 +41,62 @@ func GetLocalTimeZone() string { return systemTimeZone } -func GetLocation(tz string) *time.Location { +func GetLocation(ctx context.Context, tz string) (*time.Location, error) { if tz == "" { - return nil + return nil, errors.New("time zone cannot be empty") } - info, err := getTimeZoneInfo(tz) + loc, err := getTimeZoneLocation(tz) if err != nil { - return nil + return nil, fmt.Errorf("failed to load time zone location for %s: %w", tz, err) } - return info.location + return loc, nil } -func GetTimeZoneData(tz, format string) []byte { +func GetTimeZoneData(ctx context.Context, tz, format string) ([]byte, error) { if tz == "" { - return nil + return nil, errors.New("time zone cannot be empty") } // only support tzif format for now // we can expand this to support other formats as needed if format != "tzif" { - return nil + return nil, fmt.Errorf("unsupported time zone format: %s", format) + } + + data, err := getTimeZoneData(tz) + if err != nil { + return nil, fmt.Errorf("failed to load time zone data for %s: %w", tz, err) } - info, err := getTimeZoneInfo(tz) + return data, nil +} + +func getTimeZoneLocation(tz string) (*time.Location, error) { + if loc, ok := tzLocationCache.Load(tz); ok { + return loc, nil + } + + loc, err := time.LoadLocation(tz) if err != nil { - return nil + return nil, err } - return info.data + tzLocationCache.Store(tz, loc) + return loc, nil } -func getTimeZoneInfo(tz string) (*tzInfo, error) { - if info, ok := tzCache.Load(tz); ok { - return info, nil +func getTimeZoneData(tz string) ([]byte, error) { + if data, ok := tzDataCache.Load(tz); ok { + return data, nil } - info, err := loadTimeZoneInfo(tz) + data, err := loadTimeZoneData(tz) if err != nil { return nil, err } - tzCache.Store(tz, info) - return info, nil + tzDataCache.Store(tz, data) + return data, nil } diff --git a/runtime/timezones/tzdata_other.go b/runtime/timezones/tzdata_other.go index 9231cc7a2..cfc1d6067 100644 --- a/runtime/timezones/tzdata_other.go +++ b/runtime/timezones/tzdata_other.go @@ -17,27 +17,23 @@ import ( "os" "path" "strings" - "time" ) -func loadTimeZoneInfo(tz string) (*tzInfo, error) { +func loadTimeZoneData(tz string) ([]byte, error) { tzFile := "/usr/share/zoneinfo/" + tz if _, err := os.Stat(tzFile); err != nil { - return nil, fmt.Errorf("could not find timezone file: %v", err) + return nil, fmt.Errorf("could not find time zone file: %v", err) } - bytes, err := os.ReadFile(tzFile) + data, err := os.ReadFile(tzFile) if err != nil { - return nil, fmt.Errorf("could not read timezone file: %v", err) + return nil, fmt.Errorf("could not read time zone file: %v", err) } - loc, err := time.LoadLocationFromTZData(tz, bytes) - if err != nil { - return nil, fmt.Errorf("could not load timezone data: %v", err) + if len(data) == 0 { + return nil, errors.New("time zone data is empty") } - - info := &tzInfo{loc, bytes} - return info, nil + return data, nil } func getSystemLocalTimeZone() (string, error) { diff --git a/runtime/timezones/tzdata_windows.go b/runtime/timezones/tzdata_windows.go index 1a3f39bda..bb7500bcf 100644 --- a/runtime/timezones/tzdata_windows.go +++ b/runtime/timezones/tzdata_windows.go @@ -12,8 +12,8 @@ package timezones import ( + "errors" "fmt" - "time" "unsafe" _ "time/tzdata" @@ -27,22 +27,18 @@ import ( //go:linkname loadFromEmbeddedTZData time/tzdata.loadFromEmbeddedTZData func loadFromEmbeddedTZData(string) (string, error) -func loadTimeZoneInfo(tz string) (*tzInfo, error) { - +func loadTimeZoneData(tz string) ([]byte, error) { var data []byte if s, err := loadFromEmbeddedTZData(tz); err != nil { - return nil, fmt.Errorf("could not load timezone data: %v", err) + return nil, fmt.Errorf("could not load time zone data: %v", err) } else { data = []byte(s) } - loc, err := time.LoadLocationFromTZData(tz, data) - if err != nil { - return nil, fmt.Errorf("could not load timezone data: %v", err) + if len(data) == 0 { + return nil, errors.New("time zone data is empty") } - - info := &tzInfo{loc, data} - return info, nil + return data, nil } func getSystemLocalTimeZone() (string, error) {