[TASK] add nessasary attributes + timeout on locked goods
This commit is contained in:
parent
84f060cb3b
commit
9b10999cde
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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{}{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
Reference in New Issue