262 lines
8.0 KiB
Go
262 lines
8.0 KiB
Go
// Copyright 2018 PingCAP, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package timeutil
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/pingcap/tidb/pkg/types"
|
|
"github.com/pingcap/tidb/pkg/util/logutil"
|
|
"go.uber.org/atomic"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// init initializes `locCache`.
|
|
func init() {
|
|
// We need set systemTZ when it is in testing process.
|
|
if systemTZ.Load() == "" {
|
|
systemTZ.Store("System")
|
|
}
|
|
locCa = &locCache{}
|
|
locCa.locMap = make(map[string]*time.Location)
|
|
}
|
|
|
|
// locCa is a simple cache policy to improve the performance of 'time.LoadLocation'.
|
|
var locCa *locCache
|
|
|
|
// systemTZ is current TiDB's system timezone name.
|
|
var systemTZ atomic.String
|
|
|
|
// locCache is a simple map with lock. It stores all used timezone during the lifetime of tidb instance.
|
|
// Talked with Golang team about whether they can have some forms of cache policy available for programmer,
|
|
// they suggests that only programmers knows which one is best for their use case.
|
|
// For detail, please refer to: https://github.com/golang/go/issues/26106
|
|
type locCache struct {
|
|
// locMap stores locations used in past and can be retrieved by a timezone's name.
|
|
locMap map[string]*time.Location
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// inferOneStepLinkForPath only read one step link for the path, not like filepath.EvalSymlinks, which gets the
|
|
// recursive final linked file of the path.
|
|
func inferOneStepLinkForPath(path string) (string, error) {
|
|
fileInfo, err := os.Lstat(path)
|
|
if err != nil {
|
|
return path, err
|
|
}
|
|
if fileInfo.Mode()&os.ModeSymlink != 0 {
|
|
path, err = os.Readlink(path)
|
|
if err != nil {
|
|
return path, err
|
|
}
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
// InferSystemTZ reads system timezone from `TZ`, the path of the soft link of `/etc/localtime`. If both of them are failed, system timezone will be set to `UTC`.
|
|
// It is exported because we need to use it during bootstrap stage. And it should be only used at that stage.
|
|
func InferSystemTZ() string {
|
|
// consult $TZ to find the time zone to use.
|
|
// no $TZ means use the system default /etc/localtime.
|
|
// $TZ="" means use UTC.
|
|
// $TZ="foo" means use /usr/share/zoneinfo/foo.
|
|
tz, ok := syscall.Getenv("TZ")
|
|
switch {
|
|
case !ok:
|
|
path, err1 := filepath.EvalSymlinks("/etc/localtime")
|
|
if err1 == nil {
|
|
if strings.Contains(path, "posixrules") {
|
|
path, err1 = inferOneStepLinkForPath("/etc/localtime")
|
|
if err1 != nil {
|
|
logutil.BgLogger().Error("locate timezone files failed", zap.Error(err1))
|
|
return ""
|
|
}
|
|
}
|
|
name, err2 := inferTZNameFromFileName(path)
|
|
if err2 == nil {
|
|
return name
|
|
}
|
|
logutil.BgLogger().Error("infer timezone failed", zap.Error(err2))
|
|
}
|
|
logutil.BgLogger().Error("locate timezone files failed", zap.Error(err1))
|
|
case tz != "" && tz != "UTC":
|
|
_, err := time.LoadLocation(tz)
|
|
if err == nil {
|
|
return tz
|
|
}
|
|
}
|
|
return "UTC"
|
|
}
|
|
|
|
// inferTZNameFromFileName gets IANA timezone name from zoneinfo path.
|
|
// TODO: It will be refined later. This is just a quick fix.
|
|
func inferTZNameFromFileName(path string) (string, error) {
|
|
// phase1 only support read /etc/localtime which is a softlink to zoneinfo file
|
|
substr := "zoneinfo"
|
|
// macOs MoJave changes the sofe link of /etc/localtime from
|
|
// "/var/db/timezone/tz/2018e.1.0/zoneinfo/Asia/Shanghai"
|
|
// to "/usr/share/zoneinfo.default/Asia/Shanghai"
|
|
substrMojave := "zoneinfo.default"
|
|
|
|
if idx := strings.Index(path, substrMojave); idx != -1 {
|
|
return path[idx+len(substrMojave)+1:], nil
|
|
}
|
|
|
|
if idx := strings.Index(path, substr); idx != -1 {
|
|
return path[idx+len(substr)+1:], nil
|
|
}
|
|
return "", fmt.Errorf("path %s is not supported", path)
|
|
}
|
|
|
|
// SystemLocation returns time.SystemLocation's IANA timezone location. It is TiDB's global timezone location.
|
|
func SystemLocation() *time.Location {
|
|
loc, err := LoadLocation(systemTZ.Load())
|
|
if err != nil {
|
|
return time.Local
|
|
}
|
|
return loc
|
|
}
|
|
|
|
var setSysTZOnce sync.Once
|
|
|
|
// SetSystemTZ sets systemTZ by the value loaded from mysql.tidb.
|
|
func SetSystemTZ(name string) {
|
|
setSysTZOnce.Do(func() {
|
|
systemTZ.Store(name)
|
|
})
|
|
}
|
|
|
|
// GetSystemTZ gets the value of systemTZ, an error is returned if systemTZ is not properly set.
|
|
func GetSystemTZ() (string, error) {
|
|
systemTZ := systemTZ.Load()
|
|
if systemTZ == "System" || systemTZ == "" {
|
|
return "", fmt.Errorf("variable `systemTZ` is not properly set")
|
|
}
|
|
return systemTZ, nil
|
|
}
|
|
|
|
// getLoc first trying to load location from a cache map. If nothing found in such map, then call
|
|
// `time.LoadLocation` to get a timezone location. After trying both way, an error will be returned
|
|
//
|
|
// if valid Location is not found.
|
|
func (lm *locCache) getLoc(name string) (*time.Location, error) {
|
|
if name == "System" {
|
|
return time.Local, nil
|
|
}
|
|
lm.mu.RLock()
|
|
v, ok := lm.locMap[name]
|
|
lm.mu.RUnlock()
|
|
if ok {
|
|
return v, nil
|
|
}
|
|
|
|
if loc, err := time.LoadLocation(name); err == nil {
|
|
// assign value back to map
|
|
lm.mu.Lock()
|
|
lm.locMap[name] = loc
|
|
lm.mu.Unlock()
|
|
return loc, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("invalid name for timezone %s", name)
|
|
}
|
|
|
|
// LoadLocation loads time.Location by IANA timezone time.
|
|
func LoadLocation(name string) (*time.Location, error) {
|
|
return locCa.getLoc(name)
|
|
}
|
|
|
|
// Zone returns the current timezone name and timezone offset in seconds.
|
|
// In compatible with MySQL, we change `SystemLocation` to `System`.
|
|
func Zone(loc *time.Location) (string, int64) {
|
|
_, offset := time.Now().In(loc).Zone()
|
|
name := loc.String()
|
|
// when we found name is "System", we have no choice but push down
|
|
// "System" to TiKV side.
|
|
if name == "Local" {
|
|
name = "System"
|
|
}
|
|
|
|
return name, int64(offset)
|
|
}
|
|
|
|
// ConstructTimeZone constructs timezone by name first. When the timezone name
|
|
// is set, the daylight saving problem must be considered. Otherwise the
|
|
// timezone offset in seconds east of UTC is used to constructed the timezone.
|
|
func ConstructTimeZone(name string, offset int) (*time.Location, error) {
|
|
if name != "" {
|
|
return LoadLocation(name)
|
|
}
|
|
return time.FixedZone("", offset), nil
|
|
}
|
|
|
|
// WithinDayTimePeriod tests whether `now` is between `start` and `end`.
|
|
func WithinDayTimePeriod(start, end, now time.Time) bool {
|
|
// Converts to UTC and only keeps the hour and minute info.
|
|
start, end, now = start.UTC(), end.UTC(), now.UTC()
|
|
start = time.Date(0, 0, 0, start.Hour(), start.Minute(), 0, 0, time.UTC)
|
|
end = time.Date(0, 0, 0, end.Hour(), end.Minute(), 0, 0, time.UTC)
|
|
now = time.Date(0, 0, 0, now.Hour(), now.Minute(), 0, 0, time.UTC)
|
|
// for cases like from 00:00 to 06:00
|
|
if end.Sub(start) >= 0 {
|
|
return now.Sub(start) >= 0 && now.Sub(end) <= 0
|
|
}
|
|
// for cases like from 22:00 to 06:00
|
|
return now.Sub(end) <= 0 || now.Sub(start) >= 0
|
|
}
|
|
|
|
// ParseTimeZone parses the time zone string, returns the location and whether the time zone is valid.
|
|
func ParseTimeZone(s string) (*time.Location, error) {
|
|
if strings.EqualFold(s, "SYSTEM") {
|
|
return SystemLocation(), nil
|
|
}
|
|
|
|
loc, err := time.LoadLocation(s)
|
|
if err == nil {
|
|
return loc, nil
|
|
}
|
|
|
|
// The value can be given as a string indicating an offset from UTC, such as '+10:00' or '-6:00'.
|
|
// The time zone's value should in [-12:59,+14:00].
|
|
if strings.HasPrefix(s, "+") || strings.HasPrefix(s, "-") {
|
|
d, _, err := types.ParseDuration(types.DefaultStmtNoWarningContext, s[1:], 0)
|
|
if err == nil {
|
|
if s[0] == '-' {
|
|
if d.Duration > 12*time.Hour+59*time.Minute {
|
|
return nil, ErrUnknownTimeZone.GenWithStackByArgs(s)
|
|
}
|
|
} else {
|
|
if d.Duration > 14*time.Hour {
|
|
return nil, ErrUnknownTimeZone.GenWithStackByArgs(s)
|
|
}
|
|
}
|
|
|
|
ofst := int(d.Duration / time.Second)
|
|
if s[0] == '-' {
|
|
ofst = -ofst
|
|
}
|
|
return time.FixedZone("", ofst), nil
|
|
}
|
|
}
|
|
|
|
return nil, ErrUnknownTimeZone.GenWithStackByArgs(s)
|
|
}
|