From 9b10999cde8772614ed762c16c000625547692cf Mon Sep 17 00:00:00 2001 From: Martin Geno Date: Tue, 4 Apr 2017 19:28:46 +0200 Subject: [PATCH] [TASK] add nessasary attributes + timeout on locked goods --- cmd/stock/main.go | 4 ++- config_example.conf | 4 +++ http/status.go | 6 ++-- http/status_test.go | 6 ++-- lib/database/main.go | 10 ++++--- lib/database/main_test.go | 14 ++++++++-- models/config.go | 10 +++++-- models/duration.go | 56 +++++++++++++++++++++++++++++++++++++ models/duration_test.go | 47 +++++++++++++++++++++++++++++++ models/good.go | 40 +++++++++++++++++++++++++- models/good_release.go | 51 +++++++++++++++++++++++++++++++++ models/good_release_test.go | 41 +++++++++++++++++++++++++++ models/good_test.go | 24 ++++++++++++++++ models/product.go | 35 ++--------------------- models/product_cache.go | 43 ++++++++++++++++++++++++++++ models/product_test.go | 4 +-- 16 files changed, 342 insertions(+), 53 deletions(-) create mode 100644 models/duration.go create mode 100644 models/duration_test.go create mode 100644 models/good_release.go create mode 100644 models/good_release_test.go create mode 100644 models/good_test.go create mode 100644 models/product_cache.go diff --git a/cmd/stock/main.go b/cmd/stock/main.go index 35c2c38..6f7442b 100644 --- a/cmd/stock/main.go +++ b/cmd/stock/main.go @@ -33,7 +33,8 @@ func main() { if err != nil { log.Log.Panic(err) } - + grw := models.NewGoodReleaseWorker(config.GoodRelease) + go grw.Start() // Startwebsrver router := goji.NewMux() web.BindAPI(router) @@ -50,6 +51,7 @@ func main() { // Stop services srv.Close() + grw.Close() database.Close() log.Log.Info("received", sig) diff --git a/config_example.conf b/config_example.conf index 9519e5e..6581a6d 100644 --- a/config_example.conf +++ b/config_example.conf @@ -6,3 +6,7 @@ type = "sqlite3" connection = "file::memory:?mode=memory&cache=shared" # For Master-Slave cluster # read_connection = "" + +[good_release] +timer = "5m" +after = "30m" diff --git a/http/status.go b/http/status.go index 5f0631a..751c702 100644 --- a/http/status.go +++ b/http/status.go @@ -11,11 +11,11 @@ import ( func status(w http.ResponseWriter, r *http.Request) { log := logger.HTTP(r) - var good []*models.Good + var goods []*models.Good var count int64 var avg float64 - database.Read.Find(&good).Count(&count) //.Avg(&avg) - database.Read.Raw("SELECT avg(g.gcount) as avg FROM (select count(*) as gcount FROM good g GROUP BY g.product_id) g").Row().Scan(&avg) + database.Read.Find(&goods).Count(&count) + database.Read.Raw("SELECT avg(g.gcount) as avg FROM (select count(*) as gcount FROM good g WHERE deleted_at is NULL GROUP BY g.product_id) g").Row().Scan(&avg) lib.Write(w, map[string]interface{}{ "status": "running", "database": map[string]interface{}{ diff --git a/http/status_test.go b/http/status_test.go index 91517a7..34f9037 100644 --- a/http/status_test.go +++ b/http/status_test.go @@ -17,15 +17,15 @@ func TestStatus(t *testing.T) { database.Write.Create(&models.Good{ ProductID: 3, - Comment: "regal 1", + Position: "regal 1", }) database.Write.Create(&models.Good{ ProductID: 3, - Comment: "regal 2", + Position: "regal 2", }) database.Write.Create(&models.Good{ ProductID: 1, - Comment: "regal 10", + Position: "regal 10", }) r, w := session.JSONRequest("GET", "/api/status", nil) diff --git a/lib/database/main.go b/lib/database/main.go index 60089a1..8a7e2f0 100644 --- a/lib/database/main.go +++ b/lib/database/main.go @@ -26,17 +26,20 @@ func Open(c Config) (err error) { writeLog := log.Log.WithField("db", "write") config = &c Write, err = gorm.Open(config.Type, config.Connection) + if err != nil { + return + } Write.SingularTable(true) Write.LogMode(c.Logging) Write.SetLogger(writeLog) Write.Callback().Create().Remove("gorm:update_time_stamp") Write.Callback().Update().Remove("gorm:update_time_stamp") - if err != nil { - return - } if len(config.ReadConnection) > 0 { readLog := log.Log.WithField("db", "read") Read, err = gorm.Open(config.Type, config.ReadConnection) + if err != nil { + return + } Read.SingularTable(true) Read.LogMode(c.Logging) Read.SetLogger(readLog) @@ -52,7 +55,6 @@ func Open(c Config) (err error) { func Close() { Write.Close() Write = nil - if len(config.ReadConnection) > 0 { Read.Close() } diff --git a/lib/database/main_test.go b/lib/database/main_test.go index c397d4c..babf5fb 100644 --- a/lib/database/main_test.go +++ b/lib/database/main_test.go @@ -45,8 +45,17 @@ func TestOpenOneDB(t *testing.T) { func TestOpenTwoDB(t *testing.T) { assert := assert.New(t) AddModel(&TestModel{}) - c := Config{ + Type: "sqlite3", + Logging: true, + Connection: "file:database?mode=memory", + ReadConnection: "file/", + } + + err := Open(c) + assert.Error(err, "no error found") + + c = Config{ Type: "sqlite3", Logging: true, Connection: "file:database?mode=memory", @@ -54,7 +63,7 @@ func TestOpenTwoDB(t *testing.T) { } var count int64 - err := Open(c) + err = Open(c) assert.NoError(err, "no error") Write.Create(&TestModel{Value: "first"}) @@ -67,5 +76,4 @@ func TestOpenTwoDB(t *testing.T) { result := Read.Find(&list) assert.Error(result.Error, "error, because it is the wrong database") Close() - } diff --git a/models/config.go b/models/config.go index 88b4636..de21b2b 100644 --- a/models/config.go +++ b/models/config.go @@ -3,7 +3,7 @@ package models import ( "io/ioutil" - "github.com/influxdata/toml" + "github.com/BurntSushi/toml" "github.com/genofire/hs_master-kss-monolith/lib/database" "github.com/genofire/hs_master-kss-monolith/lib/log" @@ -11,8 +11,12 @@ import ( //Config the config File of this daemon type Config struct { - WebserverBind string - Database database.Config + WebserverBind string `toml:"webserver_bind"` + Database database.Config `toml:"database"` + GoodRelease struct { + After Duration `toml:"after"` + Timer Duration `toml:"timer"` + } `toml:"good_release"` } // ReadConfigFile reads a config model from path of a yml file diff --git a/models/duration.go b/models/duration.go new file mode 100644 index 0000000..965e528 --- /dev/null +++ b/models/duration.go @@ -0,0 +1,56 @@ +package models + +import ( + "fmt" + "strconv" + "time" +) + +// Duration is a TOML datatype +// A duration string is a possibly signed sequence of +// decimal numbers and a unit suffix, +// such as "300s", "1.5h" or "5d". +// Valid time units are "s", "m", "h", "d", "w". +type Duration struct { + time.Duration +} + +// UnmarshalTOML parses a duration string. +func (d *Duration) UnmarshalTOML(dataInterface interface{}) error { + var data string + switch dataInterface.(type) { + case string: + data = dataInterface.(string) + default: + return fmt.Errorf("invalid duration: \"%s\"", dataInterface) + } + // " + int + unit + " + if len(data) < 2 { + return fmt.Errorf("invalid duration: \"%s\"", data) + } + + unit := data[len(data)-1] + value, err := strconv.Atoi(string(data[:len(data)-1])) + if err != nil { + return fmt.Errorf("unable to parse duration %s: %s", data, err) + } + + switch unit { + case 's': + d.Duration = time.Duration(value) * time.Second + case 'm': + d.Duration = time.Duration(value) * time.Minute + case 'h': + d.Duration = time.Duration(value) * time.Hour + case 'd': + d.Duration = time.Duration(value) * time.Hour * 24 + case 'w': + d.Duration = time.Duration(value) * time.Hour * 24 * 7 + case 'y': + d.Duration = time.Duration(value) * time.Hour * 24 * 365 + default: + return fmt.Errorf("invalid duration unit: %s", string(unit)) + } + + return nil +} diff --git a/models/duration_test.go b/models/duration_test.go new file mode 100644 index 0000000..7496877 --- /dev/null +++ b/models/duration_test.go @@ -0,0 +1,47 @@ +package models + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDuration(t *testing.T) { + assert := assert.New(t) + + var tests = []struct { + input string + err string + duration time.Duration + }{ + {"", "invalid duration: \"\"", 0}, + {"1x", "invalid duration unit: x", 0}, + {"1s", "", time.Second}, + {"73s", "", time.Second * 73}, + {"1m", "", time.Minute}, + {"73m", "", time.Minute * 73}, + {"1h", "", time.Hour}, + {"43h", "", time.Hour * 43}, + {"1d", "", time.Hour * 24}, + {"8d", "", time.Hour * 24 * 8}, + {"1w", "", time.Hour * 24 * 7}, + {"52w", "", time.Hour * 24 * 7 * 52}, + {"1y", "", time.Hour * 24 * 365}, + {"3y", "", time.Hour * 24 * 365 * 3}, + } + + for _, test := range tests { + + d := Duration{} + err := d.UnmarshalTOML(test.input) + duration := d.Duration + + if test.err == "" { + assert.NoError(err) + assert.Equal(test.duration, duration) + } else { + assert.EqualError(err, test.err) + } + } +} diff --git a/models/good.go b/models/good.go index e72f081..e774f8b 100644 --- a/models/good.go +++ b/models/good.go @@ -1,11 +1,49 @@ package models -import "github.com/genofire/hs_master-kss-monolith/lib/database" +import ( + "errors" + "time" + + "github.com/jinzhu/gorm" + + "github.com/genofire/hs_master-kss-monolith/lib/database" +) type Good struct { ID int64 ProductID int64 + Position string Comment string + FouledAt *time.Time + + RecievedAt *time.Time `sql:"default:current_timestamp"` + // Make it temporary unusable + LockedAt *time.Time + LockedSecret string + // Make it unusable + DeletedAt *time.Time + Sended bool +} + +func (g *Good) FilterAvailable(db *gorm.DB) *gorm.DB { + return db.Where("locked_secret is NULL deleted_at is NULL and send_at is NULL") +} + +func (g *Good) Lock(secret string) { + now := time.Now() + g.LockedSecret = secret + g.LockedAt = &now +} +func (g *Good) IsLock() bool { + return len(g.LockedSecret) > 0 +} +func (g *Good) Unlock(secret string) error { + if g.LockedSecret == secret { + g.LockedSecret = "" + g.LockedAt = nil + return nil + } + return errors.New("wrong secret") } func init() { diff --git a/models/good_release.go b/models/good_release.go new file mode 100644 index 0000000..ccb4623 --- /dev/null +++ b/models/good_release.go @@ -0,0 +1,51 @@ +package models + +import ( + "time" + + "github.com/genofire/hs_master-kss-monolith/lib/database" + "github.com/genofire/hs_master-kss-monolith/lib/log" +) + +type GoodReleaseConfig struct { + After Duration `toml:"after"` + Timer Duration `toml:"timer"` +} + +type GoodReleaseWorker struct { + unlockTimer time.Duration + unlockAfter time.Duration + quit chan struct{} +} + +func NewGoodReleaseWorker(grc GoodReleaseConfig) (rw *GoodReleaseWorker) { + rw = &GoodReleaseWorker{ + unlockTimer: grc.Timer.Duration, + unlockAfter: grc.After.Duration, + quit: make(chan struct{}), + } + return +} + +func (rw *GoodReleaseWorker) Start() { + ticker := time.NewTicker(rw.unlockTimer) + for { + select { + case <-ticker.C: + count := goodRelease(rw.unlockAfter) + log.Log.WithField("count", count).Info("goods released") + case <-rw.quit: + ticker.Stop() + return + } + } +} + +func (rw *GoodReleaseWorker) Close() { + close(rw.quit) +} + +func goodRelease(unlockAfter time.Duration) int64 { + res := database.Write.Model(&Good{}).Where("locked_secret is not NULL and locked_at < ?", time.Now().Add(-unlockAfter)).Updates(map[string]interface{}{"locked_secret": "", "locked_at": nil}) + return res.RowsAffected +} diff --git a/models/good_release_test.go b/models/good_release_test.go new file mode 100644 index 0000000..7c4ed8f --- /dev/null +++ b/models/good_release_test.go @@ -0,0 +1,41 @@ +package models + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/genofire/hs_master-kss-monolith/lib/database" +) + +func TestGoodRelease(t *testing.T) { + assert := assert.New(t) + database.Open(database.Config{ + Type: "sqlite3", + Logging: true, + Connection: ":memory:", + }) + now := time.Now() + good := Good{ + LockedAt: &now, + LockedSecret: "never used", + } + database.Write.Create(&good) + count := goodRelease(time.Duration(3) * time.Second) + assert.Equal(int64(0), count, "no locked in timeout") + + older := now.Add(-time.Duration(10) * time.Minute) + good.LockedAt = &older + database.Write.Save(&good) + count = goodRelease(time.Duration(3) * time.Second) + assert.Equal(int64(1), count, "unlock after timeout") + + grw := NewGoodReleaseWorker(GoodReleaseConfig{ + Timer: Duration{Duration: time.Duration(3) * time.Millisecond}, + After: Duration{Duration: time.Duration(5) * time.Millisecond}, + }) + go grw.Start() + time.Sleep(time.Duration(15) * time.Millisecond) + grw.Close() +} diff --git a/models/good_test.go b/models/good_test.go new file mode 100644 index 0000000..1d0163c --- /dev/null +++ b/models/good_test.go @@ -0,0 +1,24 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGood(t *testing.T) { + assert := assert.New(t) + + good := &Good{} + assert.False(good.IsLock()) + + good.Lock("blub_secret") + assert.True(good.IsLock()) + + err := good.Unlock("secret") + assert.Error(err) + assert.True(good.IsLock()) + + good.Unlock("blub_secret") + assert.False(good.IsLock()) +} diff --git a/models/product.go b/models/product.go index 94d7ef0..5199b0c 100644 --- a/models/product.go +++ b/models/product.go @@ -1,36 +1,5 @@ package models -import ( - "net/http" - "time" -) - -type boolMicroServiceCache struct { - LastCheck time.Time - Value bool -} - -var productExistCache map[int64]boolMicroServiceCache - -func init() { - productExistCache = make(map[int64]boolMicroServiceCache) -} - -func ProductExists(id int64) (bool, error) { - if cache, ok := productExistCache[id]; ok { - // cache for 5min - before := time.Now().Add(-time.Minute * 5) - if !cache.LastCheck.Before(before) { - return cache.Value, nil - } - } - - // TODO DRAFT for a rest request to a other microservice - res, err := http.Get("http://golang.org") - - productExistCache[id] = boolMicroServiceCache{ - LastCheck: time.Now(), - Value: (res.StatusCode == http.StatusOK), - } - return productExistCache[id].Value, err +type Product struct { + ID int64 } diff --git a/models/product_cache.go b/models/product_cache.go new file mode 100644 index 0000000..19061e1 --- /dev/null +++ b/models/product_cache.go @@ -0,0 +1,43 @@ +package models + +import ( + "fmt" + "net/http" + "time" + + "github.com/genofire/hs_master-kss-monolith/lib/log" +) + +// TODO DRAFT for a rest request to a other microservice +const ProductURL = "https://google.com/?q=%d" + +type boolMicroServiceCache struct { + LastCheck time.Time + Value bool +} + +var productExistCache map[int64]boolMicroServiceCache + +func init() { + productExistCache = make(map[int64]boolMicroServiceCache) +} + +func (p *Product) Exists() (bool, error) { + if cache, ok := productExistCache[p.ID]; ok { + // cache for 5min + before := time.Now().Add(-time.Minute * 5) + if !cache.LastCheck.Before(before) { + return cache.Value, nil + } + } + + url := fmt.Sprintf(ProductURL, p.ID) + log.Log.WithField("url", url).Info("exists product?") + res, err := http.Get(url) + + productExistCache[p.ID] = boolMicroServiceCache{ + LastCheck: time.Now(), + Value: (res.StatusCode == http.StatusOK), + } + return productExistCache[p.ID].Value, err +} diff --git a/models/product_test.go b/models/product_test.go index 0693bd0..e069658 100644 --- a/models/product_test.go +++ b/models/product_test.go @@ -9,12 +9,12 @@ import ( func TestProductExists(t *testing.T) { assert := assert.New(t) - ok, err := ProductExists(3) + ok, err := (&Product{ID: 3}).Exists() assert.True(ok) assert.NoError(err) // test cache - ok, err = ProductExists(3) + ok, err = (&Product{ID: 3}).Exists() assert.True(ok) assert.NoError(err)