genofire/hs_monolith
genofire
/
hs_monolith
Archived
1
0
Fork 0

[TASK] add nessasary attributes + timeout on locked goods

This commit is contained in:
Martin Geno 2017-04-04 19:28:46 +02:00
parent 84f060cb3b
commit 9b10999cde
No known key found for this signature in database
GPG Key ID: F0D39A37E925E941
16 changed files with 342 additions and 53 deletions

View File

@ -33,7 +33,8 @@ func main() {
if err != nil { if err != nil {
log.Log.Panic(err) log.Log.Panic(err)
} }
grw := models.NewGoodReleaseWorker(config.GoodRelease)
go grw.Start()
// Startwebsrver // Startwebsrver
router := goji.NewMux() router := goji.NewMux()
web.BindAPI(router) web.BindAPI(router)
@ -50,6 +51,7 @@ func main() {
// Stop services // Stop services
srv.Close() srv.Close()
grw.Close()
database.Close() database.Close()
log.Log.Info("received", sig) log.Log.Info("received", sig)

View File

@ -6,3 +6,7 @@ type = "sqlite3"
connection = "file::memory:?mode=memory&cache=shared" connection = "file::memory:?mode=memory&cache=shared"
# For Master-Slave cluster # For Master-Slave cluster
# read_connection = "" # read_connection = ""
[good_release]
timer = "5m"
after = "30m"

View File

@ -11,11 +11,11 @@ import (
func status(w http.ResponseWriter, r *http.Request) { func status(w http.ResponseWriter, r *http.Request) {
log := logger.HTTP(r) log := logger.HTTP(r)
var good []*models.Good var goods []*models.Good
var count int64 var count int64
var avg float64 var avg float64
database.Read.Find(&good).Count(&count) //.Avg(&avg) database.Read.Find(&goods).Count(&count)
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.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{}{ lib.Write(w, map[string]interface{}{
"status": "running", "status": "running",
"database": map[string]interface{}{ "database": map[string]interface{}{

View File

@ -17,15 +17,15 @@ func TestStatus(t *testing.T) {
database.Write.Create(&models.Good{ database.Write.Create(&models.Good{
ProductID: 3, ProductID: 3,
Comment: "regal 1", Position: "regal 1",
}) })
database.Write.Create(&models.Good{ database.Write.Create(&models.Good{
ProductID: 3, ProductID: 3,
Comment: "regal 2", Position: "regal 2",
}) })
database.Write.Create(&models.Good{ database.Write.Create(&models.Good{
ProductID: 1, ProductID: 1,
Comment: "regal 10", Position: "regal 10",
}) })
r, w := session.JSONRequest("GET", "/api/status", nil) r, w := session.JSONRequest("GET", "/api/status", nil)

View File

@ -26,17 +26,20 @@ func Open(c Config) (err error) {
writeLog := log.Log.WithField("db", "write") writeLog := log.Log.WithField("db", "write")
config = &c config = &c
Write, err = gorm.Open(config.Type, config.Connection) Write, err = gorm.Open(config.Type, config.Connection)
if err != nil {
return
}
Write.SingularTable(true) Write.SingularTable(true)
Write.LogMode(c.Logging) Write.LogMode(c.Logging)
Write.SetLogger(writeLog) Write.SetLogger(writeLog)
Write.Callback().Create().Remove("gorm:update_time_stamp") Write.Callback().Create().Remove("gorm:update_time_stamp")
Write.Callback().Update().Remove("gorm:update_time_stamp") Write.Callback().Update().Remove("gorm:update_time_stamp")
if err != nil {
return
}
if len(config.ReadConnection) > 0 { if len(config.ReadConnection) > 0 {
readLog := log.Log.WithField("db", "read") readLog := log.Log.WithField("db", "read")
Read, err = gorm.Open(config.Type, config.ReadConnection) Read, err = gorm.Open(config.Type, config.ReadConnection)
if err != nil {
return
}
Read.SingularTable(true) Read.SingularTable(true)
Read.LogMode(c.Logging) Read.LogMode(c.Logging)
Read.SetLogger(readLog) Read.SetLogger(readLog)
@ -52,7 +55,6 @@ func Open(c Config) (err error) {
func Close() { func Close() {
Write.Close() Write.Close()
Write = nil Write = nil
if len(config.ReadConnection) > 0 { if len(config.ReadConnection) > 0 {
Read.Close() Read.Close()
} }

View File

@ -45,8 +45,17 @@ func TestOpenOneDB(t *testing.T) {
func TestOpenTwoDB(t *testing.T) { func TestOpenTwoDB(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
AddModel(&TestModel{}) AddModel(&TestModel{})
c := Config{ 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", Type: "sqlite3",
Logging: true, Logging: true,
Connection: "file:database?mode=memory", Connection: "file:database?mode=memory",
@ -54,7 +63,7 @@ func TestOpenTwoDB(t *testing.T) {
} }
var count int64 var count int64
err := Open(c) err = Open(c)
assert.NoError(err, "no error") assert.NoError(err, "no error")
Write.Create(&TestModel{Value: "first"}) Write.Create(&TestModel{Value: "first"})
@ -67,5 +76,4 @@ func TestOpenTwoDB(t *testing.T) {
result := Read.Find(&list) result := Read.Find(&list)
assert.Error(result.Error, "error, because it is the wrong database") assert.Error(result.Error, "error, because it is the wrong database")
Close() Close()
} }

View File

@ -3,7 +3,7 @@ package models
import ( import (
"io/ioutil" "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/database"
"github.com/genofire/hs_master-kss-monolith/lib/log" "github.com/genofire/hs_master-kss-monolith/lib/log"
@ -11,8 +11,12 @@ import (
//Config the config File of this daemon //Config the config File of this daemon
type Config struct { type Config struct {
WebserverBind string WebserverBind string `toml:"webserver_bind"`
Database database.Config 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 // ReadConfigFile reads a config model from path of a yml file

56
models/duration.go Normal file
View File

@ -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
}

47
models/duration_test.go Normal file
View File

@ -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)
}
}
}

View File

@ -1,11 +1,49 @@
package models 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 { type Good struct {
ID int64 ID int64
ProductID int64 ProductID int64
Position string
Comment 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() { func init() {

51
models/good_release.go Normal file
View File

@ -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
}

View File

@ -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()
}

24
models/good_test.go Normal file
View File

@ -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())
}

View File

@ -1,36 +1,5 @@
package models package models
import ( type Product struct {
"net/http" ID int64
"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
} }

43
models/product_cache.go Normal file
View File

@ -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
}

View File

@ -9,12 +9,12 @@ import (
func TestProductExists(t *testing.T) { func TestProductExists(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
ok, err := ProductExists(3) ok, err := (&Product{ID: 3}).Exists()
assert.True(ok) assert.True(ok)
assert.NoError(err) assert.NoError(err)
// test cache // test cache
ok, err = ProductExists(3) ok, err = (&Product{ID: 3}).Exists()
assert.True(ok) assert.True(ok)
assert.NoError(err) assert.NoError(err)