improve panic...recover of exit feature for package ghttp/gtimer/gfsnotify (#2000)

This commit is contained in:
John Guo 2022-07-13 20:20:38 +08:00 committed by GitHub
parent 675ae9bade
commit 98b2e8ab18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 168 additions and 140 deletions

View File

@ -8,6 +8,7 @@ on:
- develop
- personal/**
- feature/**
- enhance/**
- fix/**
pull_request:
@ -16,6 +17,7 @@ on:
- develop
- personal/**
- feature/**
- enhance/**
- fix/**
env:

View File

@ -98,6 +98,9 @@ type (
// Listening file descriptor mapping.
// The key is either "http" or "https" and the value is its FD.
listenerFdMap = map[string]string
// internalPanic is the custom panic for internal usage.
internalPanic string
)
const (
@ -119,9 +122,6 @@ const (
const (
supportedHttpMethods = "GET,PUT,POST,DELETE,PATCH,HEAD,CONNECT,OPTIONS,TRACE"
defaultMethod = "ALL"
exceptionExit = "exit"
exceptionExitAll = "exit_all"
exceptionExitHook = "exit_hook"
routeCacheDuration = time.Hour
ctxKeyForRequest = "gHttpRequestObject"
contentTypeXml = "text/xml"
@ -135,6 +135,12 @@ const (
gracefulShutdownTimeout = 5 * time.Second
)
const (
exceptionExit internalPanic = "exit"
exceptionExitAll internalPanic = "exit_all"
exceptionExitHook internalPanic = "exit_hook"
)
var (
// methodsMap stores all supported HTTP method.
// It is used for quick HTTP method searching using map.

View File

@ -71,14 +71,6 @@ func (lru *adapterMemoryLru) Pop() interface{} {
return nil
}
// Print is used for test only.
// func (lru *adapterMemoryLru) Print() {
// for _, v := range lru.list.FrontAll() {
// fmt.Printf("%v ", v)
// }
// fmt.Println()
// }
// SyncAndClear synchronizes the keys from `rawList` to `list` and `data`
// using Least Recently Used algorithm.
func (lru *adapterMemoryLru) SyncAndClear(ctx context.Context) {
@ -87,9 +79,7 @@ func (lru *adapterMemoryLru) SyncAndClear(ctx context.Context) {
return
}
// Data synchronization.
var (
alreadyExistItem interface{}
)
var alreadyExistItem interface{}
for {
if rawListItem := lru.rawList.PopFront(); rawListItem != nil {
// Deleting the key from list.
@ -104,9 +94,9 @@ func (lru *adapterMemoryLru) SyncAndClear(ctx context.Context) {
}
}
// Data cleaning up.
for i := lru.Size() - lru.cache.cap; i > 0; i-- {
if s := lru.Pop(); s != nil {
lru.cache.clearByKey(s, true)
for clearLength := lru.Size() - lru.cache.cap; clearLength > 0; clearLength-- {
if topKey := lru.Pop(); topKey != nil {
lru.cache.clearByKey(topKey, true)
}
}
}

View File

@ -137,7 +137,7 @@ func (entry *Entry) Close() {
// gcron.Entry relies on gtimer to implement a scheduled task check for gcron.Entry per second.
func (entry *Entry) checkAndRun(ctx context.Context) {
currentTime := time.Now()
if !entry.schedule.checkMeetAndUpdateLastSeconds(currentTime) {
if !entry.schedule.checkMeetAndUpdateLastSeconds(ctx, currentTime) {
intlog.Printf(
ctx,
`timely check, current time does not meet cron job "%s"`,

View File

@ -15,23 +15,22 @@ import (
"github.com/gogf/gf/v2/container/gtype"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gregex"
)
// cronSchedule is the schedule for cron job.
type cronSchedule struct {
create int64 // Created timestamp.
every int64 // Running interval in seconds.
pattern string // The raw cron pattern string.
second map[int]struct{} // Job can run in these second numbers.
minute map[int]struct{} // Job can run in these minute numbers.
hour map[int]struct{} // Job can run in these hour numbers.
day map[int]struct{} // Job can run in these day numbers.
week map[int]struct{} // Job can run in these week numbers.
month map[int]struct{} // Job can run in these moth numbers.
lastTimestamp *gtype.Int64 // Last timestamp number, for seconds fix.
createTimestamp int64 // Created timestamp in seconds.
everySeconds int64 // Running interval in seconds.
pattern string // The raw cron pattern string.
secondMap map[int]struct{} // Job can run in these second numbers.
minuteMap map[int]struct{} // Job can run in these minute numbers.
hourMap map[int]struct{} // Job can run in these hour numbers.
dayMap map[int]struct{} // Job can run in these day numbers.
weekMap map[int]struct{} // Job can run in these week numbers.
monthMap map[int]struct{} // Job can run in these moth numbers.
lastTimestamp *gtype.Int64 // Last timestamp number, for timestamp fix in some delay.
}
const (
@ -107,6 +106,7 @@ var (
// newSchedule creates and returns a schedule object for given cron pattern.
func newSchedule(pattern string) (*cronSchedule, error) {
var currentTimestamp = time.Now().Unix()
// Check if the predefined patterns.
if match, _ := gregex.MatchString(`(@\w+)\s*(\w*)\s*`, pattern); len(match) > 0 {
key := strings.ToLower(match[1])
@ -118,10 +118,10 @@ func newSchedule(pattern string) (*cronSchedule, error) {
return nil, err
}
return &cronSchedule{
create: time.Now().Unix(),
every: int64(d.Seconds()),
pattern: pattern,
lastTimestamp: gtype.NewInt64(),
createTimestamp: currentTimestamp,
everySeconds: int64(d.Seconds()),
pattern: pattern,
lastTimestamp: gtype.NewInt64(currentTimestamp),
}, nil
}
return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
@ -130,46 +130,46 @@ func newSchedule(pattern string) (*cronSchedule, error) {
// 0 0 0 1 1 2
if match, _ := gregex.MatchString(regexForCron, pattern); len(match) == 7 {
schedule := &cronSchedule{
create: time.Now().Unix(),
every: 0,
pattern: pattern,
lastTimestamp: gtype.NewInt64(),
createTimestamp: currentTimestamp,
everySeconds: 0,
pattern: pattern,
lastTimestamp: gtype.NewInt64(currentTimestamp),
}
// Second.
if m, err := parsePatternItem(match[1], 0, 59, false); err != nil {
return nil, err
} else {
schedule.second = m
schedule.secondMap = m
}
// Minute.
if m, err := parsePatternItem(match[2], 0, 59, false); err != nil {
return nil, err
} else {
schedule.minute = m
schedule.minuteMap = m
}
// Hour.
if m, err := parsePatternItem(match[3], 0, 23, false); err != nil {
return nil, err
} else {
schedule.hour = m
schedule.hourMap = m
}
// Day.
if m, err := parsePatternItem(match[4], 1, 31, true); err != nil {
return nil, err
} else {
schedule.day = m
schedule.dayMap = m
}
// Month.
if m, err := parsePatternItem(match[5], 1, 12, false); err != nil {
return nil, err
} else {
schedule.month = m
schedule.monthMap = m
}
// Week.
if m, err := parsePatternItem(match[6], 0, 6, true); err != nil {
return nil, err
} else {
schedule.week = m
schedule.weekMap = m
}
return schedule, nil
}
@ -208,6 +208,7 @@ func parsePatternItem(item string, min int, max int, allowQuestionMark bool) (ma
case 6:
// It's checking week field.
itemType = patternItemTypeWeek
case 12:
// It's checking month field.
itemType = patternItemTypeMonth
@ -268,78 +269,48 @@ func parsePatternItemValue(value string, itemType int) (int, error) {
}
// checkMeetAndUpdateLastSeconds checks if the given time `t` meets the runnable point for the job.
func (s *cronSchedule) checkMeetAndUpdateLastSeconds(t time.Time) bool {
if s.every != 0 {
func (s *cronSchedule) checkMeetAndUpdateLastSeconds(ctx context.Context, t time.Time) bool {
if s.everySeconds != 0 {
// It checks using interval.
if diff := t.Unix() - s.create; diff > 0 {
return diff%s.every == 0
secondsAfterCreated := t.Unix() - s.createTimestamp
secondsAfterCreated += int64(s.getFixedTimestampDelta(ctx, t))
if secondsAfterCreated > 0 {
return secondsAfterCreated%s.everySeconds == 0
}
return false
}
// It checks using normal cron pattern.
if _, ok := s.second[s.getFixedSecond(t)]; !ok {
if _, ok := s.secondMap[s.getFixedSecond(ctx, t)]; !ok {
return false
}
if _, ok := s.minute[t.Minute()]; !ok {
if _, ok := s.minuteMap[t.Minute()]; !ok {
return false
}
if _, ok := s.hour[t.Hour()]; !ok {
if _, ok := s.hourMap[t.Hour()]; !ok {
return false
}
if _, ok := s.day[t.Day()]; !ok {
if _, ok := s.dayMap[t.Day()]; !ok {
return false
}
if _, ok := s.month[int(t.Month())]; !ok {
if _, ok := s.monthMap[int(t.Month())]; !ok {
return false
}
if _, ok := s.week[int(t.Weekday())]; !ok {
if _, ok := s.weekMap[int(t.Weekday())]; !ok {
return false
}
return true
}
// getFixedSecond checks, fixes and returns the seconds that have delay in some seconds.
// Reference: https://github.com/golang/go/issues/14410
func (s *cronSchedule) getFixedSecond(t time.Time) int {
var (
second = t.Second()
currentTimestamp = t.Unix()
lastTimestamp = s.lastTimestamp.Val()
)
switch {
case
lastTimestamp == 0,
lastTimestamp == currentTimestamp-1:
lastTimestamp = currentTimestamp
case
lastTimestamp == currentTimestamp-2,
lastTimestamp == currentTimestamp-3,
lastTimestamp == currentTimestamp:
lastTimestamp += 1
second += 1
default:
// Too much delay, let's update the last timestamp to current one.
lastTimestamp = currentTimestamp
intlog.Printf(
context.Background(),
`too much delay, last "%d", current "%d"`,
lastTimestamp, currentTimestamp,
)
}
second %= 60
s.lastTimestamp.Set(lastTimestamp)
return second
}
// Next returns the next time this schedule is activated, greater than the given
// time. If no time can be found to satisfy the schedule, return the zero time.
func (s *cronSchedule) Next(t time.Time) time.Time {
if s.every != 0 {
diff := t.Unix() - s.create
cnt := diff/s.every + 1
return t.Add(time.Duration(cnt*s.every) * time.Second)
if s.everySeconds != 0 {
var (
diff = t.Unix() - s.createTimestamp
count = diff/s.everySeconds + 1
)
return t.Add(time.Duration(count*s.everySeconds) * time.Second)
}
// Start at the earliest possible time (the upcoming second).
@ -355,7 +326,7 @@ WRAP:
return t // who will care the job that run in five years later
}
for !s.match(s.month, int(t.Month())) {
for !s.match(s.monthMap, int(t.Month())) {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
@ -387,7 +358,7 @@ WRAP:
goto WRAP
}
}
for !s.match(s.hour, t.Hour()) {
for !s.match(s.hourMap, t.Hour()) {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
@ -398,7 +369,7 @@ WRAP:
goto WRAP
}
}
for !s.match(s.minute, t.Minute()) {
for !s.match(s.minuteMap, t.Minute()) {
if !added {
added = true
t = t.Truncate(time.Minute)
@ -409,7 +380,7 @@ WRAP:
goto WRAP
}
}
for !s.match(s.second, t.Second()) {
for !s.match(s.secondMap, t.Second()) {
if !added {
added = true
t = t.Truncate(time.Second)
@ -425,8 +396,8 @@ WRAP:
// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
func (s *cronSchedule) dayMatches(t time.Time) bool {
_, ok1 := s.day[t.Day()]
_, ok2 := s.week[int(t.Weekday())]
_, ok1 := s.dayMap[t.Day()]
_, ok2 := s.weekMap[int(t.Weekday())]
return ok1 && ok2
}

View File

@ -0,0 +1,53 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gcron
import (
"context"
"time"
"github.com/gogf/gf/v2/internal/intlog"
)
// getFixedSecond checks, fixes and returns the seconds that have delay fix in some seconds.
// Reference: https://github.com/golang/go/issues/14410
func (s *cronSchedule) getFixedSecond(ctx context.Context, t time.Time) int {
return (t.Second() + s.getFixedTimestampDelta(ctx, t)) % 60
}
// getFixedTimestampDelta checks, fixes and returns the timestamp delta that have delay fix in some seconds.
// The tolerated timestamp delay is `3` seconds in default.
func (s *cronSchedule) getFixedTimestampDelta(ctx context.Context, t time.Time) int {
var (
currentTimestamp = t.Unix()
lastTimestamp = s.lastTimestamp.Val()
delta int
)
switch {
case
lastTimestamp == currentTimestamp-1:
lastTimestamp = currentTimestamp
case
lastTimestamp == currentTimestamp-2,
lastTimestamp == currentTimestamp-3,
lastTimestamp == currentTimestamp:
lastTimestamp += 1
delta = 1
default:
// Too much delay, let's update the last timestamp to current one.
intlog.Printf(
ctx,
`too much delay, last timestamp "%d", current "%d"`,
lastTimestamp, currentTimestamp,
)
lastTimestamp = currentTimestamp
}
s.lastTimestamp.Set(lastTimestamp)
return delta
}

View File

@ -29,7 +29,7 @@ func TestSlash(t *testing.T) {
t.Fatal(err)
}
t.AssertEQ(sched.week, c.expected)
t.AssertEQ(sched.weekMap, c.expected)
}
})

View File

@ -56,6 +56,9 @@ type Event struct {
// Op is the bits union for file operations.
type Op uint32
// internalPanic is the custom panic for internal usage.
type internalPanic string
const (
CREATE Op = 1 << iota
WRITE
@ -65,8 +68,8 @@ const (
)
const (
repeatEventFilterDuration = time.Millisecond // Duration for repeated event filter.
callbackExitEventPanicStr = "exit" // Custom exit event for internal usage.
repeatEventFilterDuration = time.Millisecond // Duration for repeated event filter.
callbackExitEventPanicStr internalPanic = "exit" // Custom exit event for internal usage.
)
var (

View File

@ -44,14 +44,17 @@ type TimerOptions struct {
Interval time.Duration // Interval is the interval escaped of the timer.
}
// internalPanic is the custom panic for internal usage.
type internalPanic string
const (
StatusReady = 0 // Job or Timer is ready for running.
StatusRunning = 1 // Job or Timer is already running.
StatusStopped = 2 // Job or Timer is stopped.
StatusClosed = -1 // Job or Timer is closed and waiting to be deleted.
panicExit = "exit" // panicExit is used for custom job exit with panic.
defaultTimerInterval = "100" // defaultTimerInterval is the default timer interval in milliseconds.
commandEnvKeyForInterval = "gf.gtimer.interval" // commandEnvKeyForInterval is the key for command argument or environment configuring default interval duration for timer.
StatusReady = 0 // Job or Timer is ready for running.
StatusRunning = 1 // Job or Timer is already running.
StatusStopped = 2 // Job or Timer is stopped.
StatusClosed = -1 // Job or Timer is closed and waiting to be deleted.
panicExit internalPanic = "exit" // panicExit is used for custom job exit with panic.
defaultTimerInterval = "100" // defaultTimerInterval is the default timer interval in milliseconds.
commandEnvKeyForInterval = "gf.gtimer.interval" // commandEnvKeyForInterval is the key for command argument or environment configuring default interval duration for timer.
)
var (
@ -154,11 +157,3 @@ func DelayAddOnce(ctx context.Context, delay time.Duration, interval time.Durati
func DelayAddTimes(ctx context.Context, delay time.Duration, interval time.Duration, times int, job JobFunc) {
defaultTimer.DelayAddTimes(ctx, delay, interval, times, job)
}
// Exit is used in timing job internally, which exits and marks it closed from timer.
// The timing job will be automatically removed from timer later. It uses "panic-recover"
// mechanism internally implementing this feature, which is designed for simplification
// and convenience.
func Exit() {
panic(panicExit)
}

15
os/gtimer/gtimer_exit.go Normal file
View File

@ -0,0 +1,15 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gtimer
// Exit is used in timing job internally, which exits and marks it closed from timer.
// The timing job will be automatically removed from timer later. It uses "panic-recover"
// mechanism internally implementing this feature, which is designed for simplification
// and convenience.
func Exit() {
panic(panicExit)
}

View File

@ -20,7 +20,7 @@ import (
func TestJob_Start_Stop_Close(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
job := timer.Add(ctx, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -43,7 +43,7 @@ func TestJob_Start_Stop_Close(t *testing.T) {
func TestJob_Singleton(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
job := timer.Add(ctx, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -62,7 +62,7 @@ func TestJob_Singleton(t *testing.T) {
func TestJob_SetTimes(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
job := timer.Add(ctx, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -76,7 +76,7 @@ func TestJob_SetTimes(t *testing.T) {
func TestJob_Run(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
job := timer.Add(ctx, 1000*time.Millisecond, func(ctx context.Context) {
array.Append(1)

View File

@ -18,13 +18,9 @@ import (
"github.com/gogf/gf/v2/test/gtest"
)
func New() *gtimer.Timer {
return gtimer.New()
}
func TestTimer_Add_Close(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
//fmt.Println("start", time.Now())
timer.Add(ctx, 200*time.Millisecond, func(ctx context.Context) {
@ -53,7 +49,7 @@ func TestTimer_Add_Close(t *testing.T) {
func TestTimer_Start_Stop_Close(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.Add(ctx, 1000*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -75,7 +71,7 @@ func TestTimer_Start_Stop_Close(t *testing.T) {
func TestJob_Reset(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
job := timer.AddSingleton(ctx, 500*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -93,7 +89,7 @@ func TestJob_Reset(t *testing.T) {
func TestTimer_AddSingleton(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.AddSingleton(ctx, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -109,7 +105,7 @@ func TestTimer_AddSingleton(t *testing.T) {
func TestTimer_AddOnce(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.AddOnce(ctx, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -131,7 +127,7 @@ func TestTimer_AddOnce(t *testing.T) {
func TestTimer_AddTimes(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.AddTimes(ctx, 200*time.Millisecond, 2, func(ctx context.Context) {
array.Append(1)
@ -143,7 +139,7 @@ func TestTimer_AddTimes(t *testing.T) {
func TestTimer_DelayAdd(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.DelayAdd(ctx, 200*time.Millisecond, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -157,7 +153,7 @@ func TestTimer_DelayAdd(t *testing.T) {
func TestTimer_DelayAddJob(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.DelayAddEntry(ctx, 200*time.Millisecond, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -171,7 +167,7 @@ func TestTimer_DelayAddJob(t *testing.T) {
func TestTimer_DelayAddSingleton(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.DelayAddSingleton(ctx, 200*time.Millisecond, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -187,7 +183,7 @@ func TestTimer_DelayAddSingleton(t *testing.T) {
func TestTimer_DelayAddOnce(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.DelayAddOnce(ctx, 200*time.Millisecond, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)
@ -205,7 +201,7 @@ func TestTimer_DelayAddOnce(t *testing.T) {
func TestTimer_DelayAddTimes(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.DelayAddTimes(ctx, 200*time.Millisecond, 500*time.Millisecond, 2, func(ctx context.Context) {
array.Append(1)
@ -246,24 +242,21 @@ func TestTimer_AddLessThanInterval(t *testing.T) {
func TestTimer_AddLeveledJob1(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
//glog.Print("start")
timer.DelayAdd(ctx, 1000*time.Millisecond, 1000*time.Millisecond, func(ctx context.Context) {
//glog.Print("add")
array.Append(1)
})
time.Sleep(1500 * time.Millisecond)
t.Assert(array.Len(), 0)
time.Sleep(1300 * time.Millisecond)
//glog.Print("check")
t.Assert(array.Len(), 1)
})
}
func TestTimer_Exit(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timer := New()
timer := gtimer.New()
array := garray.New(true)
timer.Add(ctx, 200*time.Millisecond, func(ctx context.Context) {
array.Append(1)