From 4efd346ce1d6d7874f9eff743a6ea4249f04aae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Wed, 13 Dec 2023 06:12:13 +0800 Subject: [PATCH] Use better caching mechanism for mtr schedules --- datasources/mtr/bus/busschedule.go | 80 +++++++++++++++++++++++++++--- utils/cache.go | 67 +++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 7 deletions(-) diff --git a/datasources/mtr/bus/busschedule.go b/datasources/mtr/bus/busschedule.go index 76a6f95..7e708fd 100644 --- a/datasources/mtr/bus/busschedule.go +++ b/datasources/mtr/bus/busschedule.go @@ -1,11 +1,14 @@ package bus import ( + "fmt" "bytes" "encoding/json" "io" + "log" "net/http" "path/filepath" + "time" "github.com/tgckpg/golifehk/utils" ) @@ -33,9 +36,27 @@ type BusStopBuses struct { Suspended string `json:"isSuspended"` } +type ScheduleStatusTime struct { + time.Time +} + type BusSchedule struct { RefreshTime int `json:"appRefreshTimeInSecond,string"` BusStops [] BusStopBuses `json:"busStop"` + StatusTime ScheduleStatusTime `json:"routeStatusTime"` + RemarksTitle string `json:"routeStatusRemarkTitle"` + // 0 = OK + // 100 = Rate limit ( Unconfirmed. Not in spec ) + Status int `json:"status,string"` +} + +func (t *ScheduleStatusTime) UnmarshalJSON(b []byte) (err error) { + date, err := time.Parse(`"2006\/01\/02 15:04"`, string(b)) + if err != nil { + return err + } + t.Time = date + return } func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) { @@ -48,7 +69,6 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) { postLang := "en" if lang == "zh-Hant" { postLang = "zh" - } QUERY_FUNC := func() ( io.ReadCloser, error ) { @@ -68,16 +88,62 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) { return resp.Body, nil } - buff, err := utils.CacheStream( CACHE_PATH, QUERY_FUNC, 60 ) + cs, err := utils.CacheStreamEx( CACHE_PATH, QUERY_FUNC ) if err != nil { return nil, err } - schedules := BusSchedule{} - err = json.Unmarshal( buff.Bytes(), &schedules ) - if err != nil { - return nil, err + oldSch := BusSchedule{ + Status: -1, } - return &schedules, nil + if cs.Local != nil { + err = json.Unmarshal( cs.Local.Bytes(), &oldSch ) + if err != nil { + return nil, err + } + } + + newSch := BusSchedule{ + Status: -1, + } + + for i := 0; i < 3; i ++ { + + if cs.Remote != nil { + err = json.Unmarshal( cs.Remote.Bytes(), &newSch ) + if err != nil { + return nil, err + } + } + + if newSch.Status == 0 { + cs.Commit() + return &newSch, nil + } + + if oldSch.Status == 0 && cs.NotExpired( 60 ) { + log.Printf( "Using cache: %s", CACHE_PATH ) + return &oldSch, nil + } + + if oldSch.StatusTime.Time == newSch.StatusTime.Time { + log.Printf( "Using cache: %s", CACHE_PATH ) + return &oldSch, nil + } + + // First time + try again i times + err = cs.Reload() + log.Printf( "Reloading (%d): %s", i, CACHE_PATH ) + if err != nil { + err = fmt.Errorf( "Error retrieving data: %s", err ) + return nil, err + } + } + + if newSch.Status != 0 { + err = fmt.Errorf( "%s (%d)", newSch.RemarksTitle, newSch.Status ) + } + + return &newSch, err } diff --git a/utils/cache.go b/utils/cache.go index c61c404..a47a95e 100644 --- a/utils/cache.go +++ b/utils/cache.go @@ -9,6 +9,73 @@ import ( "time" ) +type CacheStreams struct { + Local *bytes.Buffer + Remote *bytes.Buffer + Path string + Query func() ( io.ReadCloser, error ) +} + +func ( cs *CacheStreams ) Commit() error { + + f, err := os.OpenFile( cs.Path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC, 0644 ) + if err != nil { + return err + } + + defer f.Close() + + _, err = io.Copy( f, bytes.NewReader( cs.Remote.Bytes() ) ) + log.Printf( "Commit: %s", cs.Path ) + return err +} + +func ( cs *CacheStreams ) Reload() error { + + s, err := cs.Query() + if err != nil { + return err + } + + defer s.Close() + + cs.Remote = bytes.NewBuffer( []byte{} ) + _, err = io.Copy( cs.Remote, s ) + + return err +} + +func ( cs *CacheStreams ) NotExpired( expires time.Duration ) bool { + + cache, err := os.Stat( cs.Path ) + + if err == nil { + expired := cache.ModTime().Add( expires * 1e9 ) + return time.Now().Before( expired ) + } + + return false +} + +func CacheStreamEx( path string, readStream func() ( io.ReadCloser, error ) ) ( *CacheStreams, error ) { + + cs := CacheStreams{} + cs.Path = path + cs.Query = readStream + + f, err := os.Open( path ) + if err == nil { + defer f.Close() + cs.Local = bytes.NewBuffer( []byte{} ) + _, err = io.Copy( cs.Local, f ) + if err != nil { + return nil, err + } + } + + return &cs, err +} + func CacheStream( path string, readStream func() ( io.ReadCloser, error ), expires time.Duration ) ( *bytes.Buffer, error ) {