|
package main |
|
|
|
import ( |
|
"errors" |
|
"log" |
|
"net/http" |
|
"strconv" |
|
"sync" |
|
//"sync/atomic" |
|
"encoding/json" |
|
|
|
_ "net/http/pprof" |
|
|
|
"github.com/jinzhu/gorm" |
|
_ "github.com/jinzhu/gorm/dialects/postgres" |
|
"github.com/pborman/uuid" |
|
) |
|
|
|
type ( |
|
// Zone модель |
|
Zone struct { |
|
ZID int `gorm:"primary_key;column:zid"` |
|
} |
|
// Assoc модель |
|
Assoc struct { |
|
BID int `gorm:"column:bid"` |
|
ZID int `gorm:"column:zid"` |
|
ECPM float64 `gorm:"column:ecpm"` |
|
} |
|
// User - модель пользователя |
|
User struct { |
|
UUID string // |
|
Zones map[int]int // current index in zone; zoneID: currentBanner |
|
M sync.RWMutex |
|
} |
|
// Banner модель |
|
Banner struct { |
|
BID int `gorm:"primary_key;column:bid"` |
|
Context string `gorm:"column:text;column:context"` |
|
} |
|
) |
|
|
|
// TableName - название таблицы для модельки Assoc |
|
func (a *Assoc) TableName() string { |
|
return "assoc" |
|
} |
|
|
|
// TableName - название таблицы для модельки Banner |
|
func (b *Banner) TableName() string { |
|
return "banners" |
|
} |
|
|
|
// TableName - название таблицы для модельки Zone |
|
func (z *Zone) TableName() string { |
|
return "zones" |
|
} |
|
|
|
var ( |
|
// БД |
|
db *gorm.DB |
|
|
|
// буфер банеров |
|
bannerBuffer map[int]string = make(map[int]string) |
|
|
|
// association buffer, sorted by eCPM |
|
assocBuffer map[int][]int = make(map[int][]int)// next banner |
|
|
|
// Буфер пользователей |
|
users map[string]*User = make(map[string]*User) // userID: *User |
|
) |
|
|
|
// GetUser - получение пользователя или генерация, если отсутствует в буфере |
|
func GetUser(rw *sync.RWMutex, id string) *User { |
|
rw.RLock() |
|
user, ok := users[id] |
|
rw.RUnlock() |
|
if ok { |
|
return user |
|
} |
|
|
|
user = &User{ |
|
UUID: uuid.New(), |
|
Zones: make(map[int]int), |
|
M: sync.RWMutex{}, |
|
} |
|
|
|
rw.Lock() |
|
users[user.UUID] = user |
|
rw.Unlock() |
|
return user |
|
} |
|
|
|
func initDatabase() { |
|
var err error |
|
db, err = gorm.Open("postgres", "user=yan password=123123 dbname=test sslmode=disable") |
|
if err != nil { |
|
log.Fatalf("cant connect to database: %+v", err) |
|
} |
|
|
|
db.AutoMigrate( |
|
&Banner{}, |
|
&Zone{}, |
|
&Assoc{}, |
|
) |
|
db.Model(&Assoc{}).AddIndex("idx_zid_ecpm", "zid", "ecpm") |
|
} |
|
|
|
func updateBanners() { |
|
var banners []Banner |
|
db.Raw(`SELECT * FROM banners;`).Scan(&banners) |
|
|
|
for _, banner := range banners { |
|
bannerBuffer[banner.BID] = banner.Context |
|
} |
|
} |
|
|
|
func updateAssoc() { |
|
var assoc []Assoc |
|
db.Raw(`SELECT * FROM assoc ORDER BY zid ASC, ecpm DESC;`).Scan(&assoc) |
|
for _, value := range assoc { |
|
assocBuffer[value.ZID] = append(assocBuffer[value.ZID], value.BID) |
|
} |
|
} |
|
|
|
// получение баннера для пользователя по запрошенной зоне |
|
func getBanner(user *User, strZoneID string) (string, error) { |
|
zoneID, err := strconv.Atoi(strZoneID) |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
user.M.RLock() |
|
currentIndex := user.Zones[zoneID] |
|
if currentIndex >= len(assocBuffer[zoneID]) { |
|
user.M.RUnlock() |
|
return "", errors.New("current zone is empty") |
|
} |
|
user.M.RUnlock() |
|
defer func() { |
|
user.M.Lock() |
|
user.Zones[zoneID] = currentIndex + 1 |
|
user.M.Unlock() |
|
}() |
|
|
|
if context, ok := bannerBuffer[assocBuffer[zoneID][currentIndex]]; ok { |
|
return context, nil |
|
} |
|
return "", errors.New("not found context") |
|
} |
|
|
|
func main() { |
|
log.Println("init database...") |
|
initDatabase() |
|
|
|
log.Println("clean fixtures into database...") |
|
db.Exec(` |
|
DELETE FROM zones; |
|
DELETE FROM banners; |
|
DELETE FROM assoc; |
|
`) |
|
|
|
log.Println("insert fixtures into database...") |
|
db.Exec(` |
|
INSERT INTO zones(zid) SELECT generate_series(1,10000) as zid; |
|
INSERT INTO banners(bid,context) SELECT generate_series(1,100000) as bid, (random()::text) as context; |
|
INSERT INTO assoc(zid,bid,ecpm) (SELECT round(random() * 10000) as zid, round(random() * 100000) as bid, random() as ecpm FROM generate_series(1,100000)); |
|
`) |
|
|
|
log.Println("update cache...") |
|
updateBanners() |
|
updateAssoc() |
|
|
|
|
|
log.Printf("size of assoc: %d", len(assocBuffer)) |
|
log.Printf("size of banners: %d", len(bannerBuffer)) |
|
|
|
m := sync.RWMutex{} |
|
mux := http.NewServeMux() |
|
mux.HandleFunc("/info", func(w http.ResponseWriter, req *http.Request) { |
|
json.NewEncoder(w).Encode(map[string]int{ |
|
"bannerBuffer": len(bannerBuffer), |
|
"assocBuffer": len(assocBuffer), |
|
"users": len(users), |
|
}) |
|
}) |
|
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { |
|
cookie, err := req.Cookie("userid") |
|
if err != nil { |
|
cookie = &http.Cookie{Name: "userid"} |
|
} |
|
user := GetUser(&m, cookie.Value) |
|
cookie.Value = user.UUID |
|
http.SetCookie(w, cookie) |
|
|
|
zoneID := req.URL.Query().Get("zid") |
|
banner, err := getBanner(user, zoneID) |
|
json.NewEncoder(w).Encode(map[string]interface{}{ |
|
"error": err, |
|
"banner": banner, |
|
"zid": zoneID, |
|
}) |
|
}) |
|
|
|
go func() { |
|
log.Println(http.ListenAndServe("localhost:6060", nil)) |
|
}() |
|
|
|
log.Println("start http server...") |
|
(&http.Server{ |
|
Handler: mux, |
|
Addr: ":3000", |
|
}).ListenAndServe() |
|
} |