Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add APIs for local time and time zones #663

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@
"label": "Text Generation Model Example",
"value": "textgeneration"
},
{
"label": "Time Example",
"value": "time"
},
{
"label": "Vectors API Example",
"value": "vectors"
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
- test: add tests for AssemblyScript SDK Transform [#659](https://github.com/hypermodeinc/modus/pull/659)
- fix: improve Go version handling [#660](https://github.com/hypermodeinc/modus/pull/660)
- fix: update runtime wasm tests [#661](https://github.com/hypermodeinc/modus/pull/661)
- fix: support TinyGo 0.35.0 [#661](https://github.com/hypermodeinc/modus/pull/662)
- fix: support TinyGo 0.35.0 [#662](https://github.com/hypermodeinc/modus/pull/662)
- feat: add APIs for local time and time zones [#663](https://github.com/hypermodeinc/modus/pull/663)

## 2024-12-13 - Runtime 0.15.0

Expand Down
6 changes: 5 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"buger",
"buildmode",
"cconf",
"checklinkname",
"chewxy",
"classid",
"codegen",
Expand Down Expand Up @@ -97,6 +98,7 @@
"Lessable",
"lestrrat",
"linkname",
"localtime",
"logit",
"logits",
"logprob",
Expand Down Expand Up @@ -176,6 +178,7 @@
"tseslint",
"tsrv",
"typedarray",
"tzif",
"uids",
"uncategorized",
"Unmarshalled",
Expand Down Expand Up @@ -209,6 +212,7 @@
"xsync",
"xxhash",
"zenquotes",
"zerolog"
"zerolog",
"zoneinfo"
]
}
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use (
./sdk/go/examples/postgresql
./sdk/go/examples/simple
./sdk/go/examples/textgeneration
./sdk/go/examples/time
./sdk/go/examples/vectors
./sdk/go/templates/default
)
9 changes: 8 additions & 1 deletion runtime/.goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ builds:
- amd64
- arm64
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags: -s -w -X github.com/hypermodeinc/modus/runtime/config.version={{.Version}}
ldflags:
- -s
- -w
- -X github.com/hypermodeinc/modus/runtime/config.version={{.Version}}
- >-
{{- if eq .Os "windows"}}
-checklinkname=0
{{- end }}

# temporarily disabled
# notarize:
Expand Down
4 changes: 3 additions & 1 deletion runtime/Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
EXECUTABLE := modus_runtime
VERSION := $(shell git describe --tags --always --match 'runtime/*' | sed 's/^runtime\///')
LDFLAGS := -s -w -X github.com/hypermodeinc/modus/runtime/config.version=$(VERSION)

ifneq ($(OS), Windows_NT)
OS := $(shell uname -s)
endif

ifeq ($(OS), Windows_NT)
EXECUTABLE := $(EXECUTABLE).exe
LDFLAGS := $(LDFLAGS) -checklinkname=0
endif

.PHONY: all
Expand All @@ -23,7 +25,7 @@ build-explorer:

.PHONY: build
build: build-explorer
go build -o $(EXECUTABLE) -ldflags "-s -w -X github.com/hypermodeinc/modus/runtime/config.version=$(VERSION)" .
go build -o $(EXECUTABLE) -ldflags "$(LDFLAGS)" .

.PHONY: run
run: build-explorer
Expand Down
2 changes: 1 addition & 1 deletion runtime/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ require (
github.com/wundergraph/graphql-go-tools/execution v1.1.0
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.136
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
golang.org/x/sys v0.28.0
google.golang.org/grpc v1.69.2
)

Expand Down Expand Up @@ -131,7 +132,6 @@ require (
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
Expand Down
13 changes: 13 additions & 0 deletions runtime/graphql/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/hypermodeinc/modus/runtime/logger"
"github.com/hypermodeinc/modus/runtime/manifestdata"
"github.com/hypermodeinc/modus/runtime/pluginmanager"
"github.com/hypermodeinc/modus/runtime/timezones"
"github.com/hypermodeinc/modus/runtime/utils"
"github.com/hypermodeinc/modus/runtime/wasmhost"

Expand Down Expand Up @@ -101,6 +102,18 @@ func handleGraphQLRequest(w http.ResponseWriter, r *http.Request) {
output := make(map[string]wasmhost.ExecutionInfo)
ctx = context.WithValue(ctx, utils.FunctionOutputContextKey, output)

// Set time zone in the context
var timeZone string
if tz := r.Header.Get("X-Time-Zone"); tz != "" {
// If the X-Time-Zone header is set in the request, use that time zone.
timeZone = tz
} else {
// Otherwise, use the host's local time zone.
// Note, the TZ environment variable can be set to override the actual local time zone.
timeZone = timezones.GetLocalTimeZone()
}
ctx = context.WithValue(ctx, utils.TimeZoneContextKey, timeZone)

// Set tracing options
var options = []eng.ExecutionOptions{}
if utils.TraceModeEnabled() {
Expand Down
30 changes: 30 additions & 0 deletions runtime/hostfunctions/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ import (
"context"
"fmt"
"os"
"time"

"github.com/hypermodeinc/modus/runtime/logger"
"github.com/hypermodeinc/modus/runtime/timezones"
"github.com/hypermodeinc/modus/runtime/utils"
)

func init() {
const module_name = "modus_system"

registerHostFunction(module_name, "logMessage", LogMessage)
registerHostFunction(module_name, "getTimeInZone", GetTimeInZone)
registerHostFunction(module_name, "getTimeZoneData", GetTimeZoneData)
}

func LogMessage(ctx context.Context, level, message string) {
Expand All @@ -46,3 +50,29 @@ func LogMessage(ctx context.Context, level, message string) {
Bool("user_visible", true).
Msg("Message logged from function.")
}

func GetTimeInZone(ctx context.Context, tz *string) *string {
now := time.Now()

var loc *time.Location
if tz != nil && *tz != "" {
loc = timezones.GetLocation(*tz)
} else if tz, ok := ctx.Value(utils.TimeZoneContextKey).(string); ok {
loc = timezones.GetLocation(tz)
}

if loc != nil {
now = now.In(loc)
}

s := now.Format(time.RFC3339Nano)
return &s
}

func GetTimeZoneData(tz, format *string) []byte {
if tz == nil {
return nil
}

return timezones.GetTimeZoneData(*tz, *format)
}
90 changes: 90 additions & 0 deletions runtime/timezones/timezones.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2024 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2024 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

package timezones

import (
"fmt"
"os"
"time"

"github.com/puzpuzpuz/xsync/v3"
)

type tzInfo struct {
location *time.Location
data []byte
}

var systemTimeZone string
var tzCache = *xsync.NewMapOf[string, *tzInfo]()

func init() {
if tz, err := getSystemLocalTimeZone(); err == nil {
systemTimeZone = tz
} else {
fmt.Fprintf(os.Stderr, "failed to determine system local time zone (using UTC): %v\n", err)
systemTimeZone = "UTC"
}
}

func GetLocalTimeZone() string {
// check every time, in case the env var has changed via .env file reload
if tz := os.Getenv("TZ"); tz != "" {
return tz
}

return systemTimeZone
}

func GetLocation(tz string) *time.Location {
if tz == "" {
return nil
}

info, err := getTimeZoneInfo(tz)
if err != nil {
return nil
}

return info.location
}

func GetTimeZoneData(tz, format string) []byte {
if tz == "" {
return nil
}

// only support tzif format for now
// we can expand this to support other formats as needed
if format != "tzif" {
return nil
}

info, err := getTimeZoneInfo(tz)
if err != nil {
return nil
}

return info.data
}

func getTimeZoneInfo(tz string) (*tzInfo, error) {
if info, ok := tzCache.Load(tz); ok {
return info, nil
}

info, err := loadTimeZoneInfo(tz)
if err != nil {
return nil, err
}

tzCache.Store(tz, info)
return info, nil
}
59 changes: 59 additions & 0 deletions runtime/timezones/tzdata_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//go:build !windows

/*
* Copyright 2024 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2024 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

package timezones

import (
"errors"
"fmt"
"os"
"path"
"strings"
"time"
)

func loadTimeZoneInfo(tz string) (*tzInfo, error) {
tzFile := "/usr/share/zoneinfo/" + tz
if _, err := os.Stat(tzFile); err != nil {
return nil, fmt.Errorf("could not find timezone file: %v", err)
}

bytes, err := os.ReadFile(tzFile)
if err != nil {
return nil, fmt.Errorf("could not read timezone file: %v", err)
}

loc, err := time.LoadLocationFromTZData(tz, bytes)
if err != nil {
return nil, fmt.Errorf("could not load timezone data: %v", err)
}

info := &tzInfo{loc, bytes}
return info, nil
}

func getSystemLocalTimeZone() (string, error) {
// On Linux and macOS, we use the system default /etc/localtime file to get the time zone.
// It is a symlink to the time zone file within the OS's copy of the IANA time zone database.
// The time zone identifier is the path to the file relative to the zoneinfo folder.

p, err := os.Readlink("/etc/localtime")
if err == nil {
segments := strings.Split(p, string(os.PathSeparator))
for i := len(segments) - 1; i >= 0; i-- {
if segments[i] == "zoneinfo" {
return path.Join(segments[i+1:]...), nil
}
}
}

return "", errors.New("failed to determine system local time zone")
}
Loading
Loading