github.com/whtcorpsinc/MilevaDB-Prod@v0.0.0-20211104133533-f57f4be3b597/soliton/timeutil/time.go (about)

     1  // Copyright 2020 WHTCORPS INC, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package timeutil
    15  
    16  import (
    17  	"fmt"
    18  	"path/filepath"
    19  	"strings"
    20  	"sync"
    21  	"syscall"
    22  	"time"
    23  
    24  	"github.com/whtcorpsinc/milevadb/soliton/logutil"
    25  	"github.com/uber-go/atomic"
    26  	"go.uber.org/zap"
    27  )
    28  
    29  // init initializes `locCache`.
    30  func init() {
    31  	// We need set systemTZ when it is in testing process.
    32  	if systemTZ.Load() == "" {
    33  		systemTZ.CausetStore("System")
    34  	}
    35  	locCa = &locCache{}
    36  	locCa.locMap = make(map[string]*time.Location)
    37  }
    38  
    39  // locCa is a simple cache policy to improve the performance of 'time.LoadLocation'.
    40  var locCa *locCache
    41  
    42  // systemTZ is current MilevaDB's system timezone name.
    43  var systemTZ atomic.String
    44  
    45  // locCache is a simple map with dagger. It stores all used timezone during the lifetime of milevadb instance.
    46  // Talked with Golang team about whether they can have some forms of cache policy available for programmer,
    47  // they suggests that only programmers knows which one is best for their use case.
    48  // For detail, please refer to: https://github.com/golang/go/issues/26106
    49  type locCache struct {
    50  	sync.RWMutex
    51  	// locMap stores locations used in past and can be retrieved by a timezone's name.
    52  	locMap map[string]*time.Location
    53  }
    54  
    55  // 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`.
    56  // It is exported because we need to use it during bootstrap stage. And it should be only used at that stage.
    57  func InferSystemTZ() string {
    58  	// consult $TZ to find the time zone to use.
    59  	// no $TZ means use the system default /etc/localtime.
    60  	// $TZ="" means use UTC.
    61  	// $TZ="foo" means use /usr/share/zoneinfo/foo.
    62  	tz, ok := syscall.Getenv("TZ")
    63  	switch {
    64  	case !ok:
    65  		path, err1 := filepath.EvalSymlinks("/etc/localtime")
    66  		if err1 == nil {
    67  			name, err2 := inferTZNameFromFileName(path)
    68  			if err2 == nil {
    69  				return name
    70  			}
    71  			logutil.BgLogger().Error("infer timezone failed", zap.Error(err2))
    72  		}
    73  		logutil.BgLogger().Error("locate timezone files failed", zap.Error(err1))
    74  	case tz != "" && tz != "UTC":
    75  		_, err := time.LoadLocation(tz)
    76  		if err == nil {
    77  			return tz
    78  		}
    79  	}
    80  	return "UTC"
    81  }
    82  
    83  // inferTZNameFromFileName gets IANA timezone name from zoneinfo path.
    84  // TODO: It will be refined later. This is just a quick fix.
    85  func inferTZNameFromFileName(path string) (string, error) {
    86  	// phase1 only support read /etc/localtime which is a softlink to zoneinfo file
    87  	substr := "zoneinfo"
    88  	// macOs MoJave changes the sofe link of /etc/localtime from
    89  	// "/var/EDB/timezone/tz/2018e.1.0/zoneinfo/Asia/Shanghai"
    90  	// to "/usr/share/zoneinfo.default/Asia/Shanghai"
    91  	substrMojave := "zoneinfo.default"
    92  
    93  	if idx := strings.Index(path, substrMojave); idx != -1 {
    94  		return path[idx+len(substrMojave)+1:], nil
    95  	}
    96  
    97  	if idx := strings.Index(path, substr); idx != -1 {
    98  		return path[idx+len(substr)+1:], nil
    99  	}
   100  	return "", fmt.Errorf("path %s is not supported", path)
   101  }
   102  
   103  // SystemLocation returns time.SystemLocation's IANA timezone location. It is MilevaDB's global timezone location.
   104  func SystemLocation() *time.Location {
   105  	loc, err := LoadLocation(systemTZ.Load())
   106  	if err != nil {
   107  		return time.Local
   108  	}
   109  	return loc
   110  }
   111  
   112  var setSysTZOnce sync.Once
   113  
   114  // SetSystemTZ sets systemTZ by the value loaded from allegrosql.milevadb.
   115  func SetSystemTZ(name string) {
   116  	setSysTZOnce.Do(func() {
   117  		systemTZ.CausetStore(name)
   118  	})
   119  }
   120  
   121  // GetSystemTZ gets the value of systemTZ, an error is returned if systemTZ is not properly set.
   122  func GetSystemTZ() (string, error) {
   123  	systemTZ := systemTZ.Load()
   124  	if systemTZ == "System" || systemTZ == "" {
   125  		return "", fmt.Errorf("variable `systemTZ` is not properly set")
   126  	}
   127  	return systemTZ, nil
   128  }
   129  
   130  // getLoc first trying to load location from a cache map. If nothing found in such map, then call
   131  // `time.LoadLocation` to get a timezone location. After trying both way, an error will be returned
   132  //  if valid Location is not found.
   133  func (lm *locCache) getLoc(name string) (*time.Location, error) {
   134  	if name == "System" {
   135  		return time.Local, nil
   136  	}
   137  	lm.RLock()
   138  	v, ok := lm.locMap[name]
   139  	lm.RUnlock()
   140  	if ok {
   141  		return v, nil
   142  	}
   143  
   144  	if loc, err := time.LoadLocation(name); err == nil {
   145  		// assign value back to map
   146  		lm.Lock()
   147  		lm.locMap[name] = loc
   148  		lm.Unlock()
   149  		return loc, nil
   150  	}
   151  
   152  	return nil, fmt.Errorf("invalid name for timezone %s", name)
   153  }
   154  
   155  // LoadLocation loads time.Location by IANA timezone time.
   156  func LoadLocation(name string) (*time.Location, error) {
   157  	return locCa.getLoc(name)
   158  }
   159  
   160  // Zone returns the current timezone name and timezone offset in seconds.
   161  // In compatible with MyALLEGROSQL, we change `SystemLocation` to `System`.
   162  func Zone(loc *time.Location) (string, int64) {
   163  	_, offset := time.Now().In(loc).Zone()
   164  	name := loc.String()
   165  	// when we found name is "System", we have no choice but push down
   166  	// "System" to EinsteinDB side.
   167  	if name == "Local" {
   168  		name = "System"
   169  	}
   170  
   171  	return name, int64(offset)
   172  }
   173  
   174  // ConstructTimeZone constructs timezone by name first. When the timezone name
   175  // is set, the daylight saving problem must be considered. Otherwise the
   176  // timezone offset in seconds east of UTC is used to constructed the timezone.
   177  func ConstructTimeZone(name string, offset int) (*time.Location, error) {
   178  	if name != "" {
   179  		return LoadLocation(name)
   180  	}
   181  	return time.FixedZone("", offset), nil
   182  }
   183  
   184  // WithinDayTimePeriod tests whether `now` is between `start` and `end`.
   185  func WithinDayTimePeriod(start, end, now time.Time) bool {
   186  	// Converts to UTC and only keeps the hour and minute info.
   187  	start, end, now = start.UTC(), end.UTC(), now.UTC()
   188  	start = time.Date(0, 0, 0, start.Hour(), start.Minute(), 0, 0, time.UTC)
   189  	end = time.Date(0, 0, 0, end.Hour(), end.Minute(), 0, 0, time.UTC)
   190  	now = time.Date(0, 0, 0, now.Hour(), now.Minute(), 0, 0, time.UTC)
   191  	// for cases like from 00:00 to 06:00
   192  	if end.Sub(start) >= 0 {
   193  		return now.Sub(start) >= 0 && now.Sub(end) <= 0
   194  	}
   195  	// for cases like from 22:00 to 06:00
   196  	return now.Sub(end) <= 0 || now.Sub(start) >= 0
   197  }