271 lines
7.4 KiB
Go
271 lines
7.4 KiB
Go
package daemon
|
||
|
||
/*
|
||
Отписка от матча:
|
||
- метод Unwatch
|
||
- когда матч исчезнет из лайва
|
||
*/
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"sync"
|
||
"time"
|
||
|
||
"gordenko.dev/dima/onexbet"
|
||
"gordenko.dev/dima/ws"
|
||
"gordenko.dev/dima/ws/publisher"
|
||
)
|
||
|
||
const (
|
||
defaultLiveMatchesLoadInterval = 60 * time.Second
|
||
defaultPollingInterval = 5 * time.Second
|
||
)
|
||
|
||
type Options struct {
|
||
Logger *log.Logger
|
||
LiveMatchesLoadInterval int
|
||
PollingInterval int
|
||
SportIDs []onexbet.OnexbetSport
|
||
WsPublisher *publisher.PublicPublisher
|
||
}
|
||
|
||
type Daemon struct {
|
||
mutex sync.Mutex
|
||
publisher *publisher.PublicPublisher
|
||
port int
|
||
liveMatchesLoadInterval time.Duration
|
||
logger *log.Logger
|
||
//liveMatches map[string]onexbet.LiveMatch
|
||
teamLiveMatches map[onexbet.OnexbetSport][]onexbet.LiveMatch
|
||
// matchID => isClosed flag
|
||
pollingMatches map[onexbet.OnexbetSport]map[string]*bool
|
||
pollingInterval time.Duration
|
||
sportIDs []onexbet.OnexbetSport
|
||
}
|
||
|
||
func New(opt Options) *Daemon {
|
||
if opt.Logger == nil {
|
||
panic("Logger option is required")
|
||
}
|
||
s := &Daemon{
|
||
logger: opt.Logger,
|
||
publisher: opt.WsPublisher,
|
||
pollingMatches: make(map[onexbet.OnexbetSport]map[string]*bool),
|
||
teamLiveMatches: make(map[onexbet.OnexbetSport][]onexbet.LiveMatch),
|
||
sportIDs: opt.SportIDs,
|
||
}
|
||
if opt.LiveMatchesLoadInterval <= 0 {
|
||
s.liveMatchesLoadInterval = defaultLiveMatchesLoadInterval
|
||
} else {
|
||
s.liveMatchesLoadInterval = time.Duration(opt.LiveMatchesLoadInterval) * time.Second
|
||
}
|
||
if opt.PollingInterval <= 0 {
|
||
s.pollingInterval = defaultPollingInterval
|
||
} else {
|
||
s.pollingInterval = time.Duration(opt.PollingInterval) * time.Second
|
||
}
|
||
for _, sportID := range s.sportIDs {
|
||
s.pollingMatches[sportID] = make(map[string]*bool)
|
||
}
|
||
return s
|
||
}
|
||
|
||
func (s *Daemon) OnWsClientConnected(conn *ws.Conn) {
|
||
s.mutex.Lock()
|
||
defer s.mutex.Unlock()
|
||
|
||
for _, sportID := range s.sportIDs {
|
||
conn.Send(onexbet.WsMsgLiveMatches, s.getLiveMatchesMessage(sportID))
|
||
}
|
||
|
||
s.publisher.SubscribeTo(conn, onexbet.TopicLive)
|
||
}
|
||
|
||
func (s *Daemon) getLiveMatchesMessage(sportID onexbet.OnexbetSport) onexbet.LiveMatchesMessage {
|
||
var watching []string
|
||
|
||
for matchID := range s.pollingMatches[sportID] {
|
||
watching = append(watching, matchID)
|
||
}
|
||
|
||
return onexbet.LiveMatchesMessage{
|
||
SportID: sportID,
|
||
Matches: s.teamLiveMatches[sportID],
|
||
Watching: watching,
|
||
}
|
||
}
|
||
|
||
func (s *Daemon) loadLiveMatches(sportID onexbet.OnexbetSport) {
|
||
s.logger.Printf("Load live matches\n")
|
||
|
||
list, err := onexbet.ListLiveMatchesBySport(sportID)
|
||
if err != nil {
|
||
s.logger.Printf("ListLiveMatchesBySport(sportID=%d): %s\n", sportID, err)
|
||
return
|
||
}
|
||
//s.logger.Printf("NewMatches: %# v\n", pretty.Formatter(newMatches))
|
||
//s.logger.Printf("NewMatches: %d\n", len(newMatchesList))
|
||
|
||
// sync current live matches with new matches
|
||
|
||
s.mutex.Lock()
|
||
defer s.mutex.Unlock()
|
||
|
||
s.teamLiveMatches[sportID] = list
|
||
|
||
s.publisher.Publish(onexbet.TopicLive, onexbet.WsMsgLiveMatches, s.getLiveMatchesMessage(sportID))
|
||
}
|
||
|
||
// Метод может прислать обновление для матча после закрытия. Это не проблема.
|
||
func (s *Daemon) pollTeamMatch(m map[string]*bool, req onexbet.WatchReq, isClosedPtr *bool) { //closeCh chan struct{}) {
|
||
s.logger.Printf("poll team match %s\n", req.MatchID)
|
||
for {
|
||
if *isClosedPtr {
|
||
s.logger.Printf("match %s polling is closed by command\n", req.MatchID)
|
||
return
|
||
}
|
||
|
||
data, isMatchFinished, err := onexbet.LoadTeamLiveMatchData(req.MatchID)
|
||
if err != nil {
|
||
s.logger.Printf("LoadTeamLiveMatchData: %s; matchID=%s", err, req.MatchID)
|
||
} else {
|
||
//s.sendLiveMatchData(data)
|
||
//s.logger.Printf("match update: %v\n\n", data)
|
||
// Мутекс нужен, чтобы защитить Publisher, ибо он НЕ ThreadSafe!
|
||
|
||
if isMatchFinished {
|
||
// Отправлять сообщение не нужно, ибо Tipper обнаружит что матч исчез из
|
||
// polling и запросит для него результат
|
||
//s.mutex.Lock()
|
||
//delete(m, req.MatchID)
|
||
//s.publisher.Publish(ChannelLive, onexbet.WSMessageMatchFinished, req)
|
||
//s.mutex.Unlock()
|
||
// выходим
|
||
s.logger.Printf("match %s polling is finished\n", req.MatchID)
|
||
return
|
||
}
|
||
|
||
s.mutex.Lock()
|
||
s.publisher.Publish(onexbet.TopicLive, onexbet.WsMsgTeamLiveMatchData, data)
|
||
s.mutex.Unlock()
|
||
}
|
||
|
||
time.Sleep(s.pollingInterval)
|
||
}
|
||
}
|
||
|
||
// Метод может прислать обновление для матча после закрытия. Это не проблема.
|
||
func (s *Daemon) pollTennisMatch(m map[string]*bool, req onexbet.WatchReq, isClosedPtr *bool) { //closeCh chan struct{}) {
|
||
s.logger.Printf("poll tennis match %s\n", req.MatchID)
|
||
for {
|
||
if *isClosedPtr {
|
||
s.logger.Printf("match %s polling is closed by command\n", req.MatchID)
|
||
return
|
||
}
|
||
|
||
data, isMatchFinished, err := onexbet.LoadTennisLiveMatchData(req.MatchID)
|
||
if err != nil {
|
||
s.logger.Printf("LoadTennisLiveMatchData: %s; matchID=%s", err, req.MatchID)
|
||
} else {
|
||
//s.sendLiveMatchData(data)
|
||
//s.logger.Printf("match update: %v\n\n", data)
|
||
// Мутекс нужен, чтобы защитить Publisher, ибо он НЕ ThreadSafe!
|
||
|
||
if isMatchFinished {
|
||
// Отправлять сообщение не нужно, ибо Tipper обнаружит что матч исчез из
|
||
// polling и запросит для него результат
|
||
//s.mutex.Lock()
|
||
//delete(m, req.MatchID)
|
||
//s.publisher.Publish(ChannelLive, onexbet.WSMessageMatchFinished, req)
|
||
//s.mutex.Unlock()
|
||
// выходим
|
||
s.logger.Printf("match %s polling is finished\n", req.MatchID)
|
||
return
|
||
}
|
||
|
||
s.mutex.Lock()
|
||
s.publisher.Publish(onexbet.TopicLive, onexbet.WsMsgTennisLiveMatchData, data)
|
||
s.mutex.Unlock()
|
||
}
|
||
|
||
time.Sleep(s.pollingInterval)
|
||
}
|
||
}
|
||
|
||
func (s *Daemon) Watch(req onexbet.WatchReq) (err error) {
|
||
s.logger.Printf("Watch: sport=%d, matchID=%s\n", req.SportID, req.MatchID)
|
||
|
||
s.mutex.Lock()
|
||
defer s.mutex.Unlock()
|
||
|
||
m, ok := s.pollingMatches[req.SportID]
|
||
if !ok {
|
||
err = fmt.Errorf("Unknown sportID: %d", req.SportID)
|
||
return
|
||
}
|
||
|
||
_, ok = m[req.MatchID]
|
||
if ok {
|
||
// already polling
|
||
s.logger.Printf("match %s already polling\n", req.MatchID)
|
||
return
|
||
}
|
||
|
||
var isClosed bool
|
||
|
||
isClosedPtr := &isClosed
|
||
|
||
m[req.MatchID] = isClosedPtr //closeCh
|
||
|
||
if req.SportID == onexbet.Tennis {
|
||
go s.pollTennisMatch(m, req, isClosedPtr)
|
||
} else {
|
||
go s.pollTeamMatch(m, req, isClosedPtr)
|
||
}
|
||
return
|
||
}
|
||
|
||
func (s *Daemon) Unwatch(req onexbet.WatchReq) (err error) {
|
||
s.logger.Printf("Unwatch: sport=%d, matchID=%s\n", req.SportID, req.MatchID)
|
||
|
||
s.mutex.Lock()
|
||
defer s.mutex.Unlock()
|
||
|
||
m, ok := s.pollingMatches[req.SportID]
|
||
if !ok {
|
||
err = fmt.Errorf("Unknown sportID: %d", req.SportID)
|
||
return
|
||
}
|
||
|
||
isClosedPtr, ok := m[req.MatchID]
|
||
if !ok {
|
||
return
|
||
}
|
||
// Сигнализируем горутине чтобы она закрылась
|
||
*isClosedPtr = true
|
||
delete(m, req.MatchID)
|
||
return
|
||
}
|
||
|
||
func (s *Daemon) Run() {
|
||
for _, sportID := range s.sportIDs {
|
||
s.loadLiveMatches(sportID)
|
||
s.logger.Println("load end")
|
||
}
|
||
|
||
s.logger.Printf("liveMatchesLoadInterval: %d\n", int(s.liveMatchesLoadInterval.Seconds()))
|
||
ticker := time.NewTicker(s.liveMatchesLoadInterval)
|
||
|
||
for {
|
||
s.logger.Println("new loop")
|
||
select {
|
||
case <-ticker.C:
|
||
for _, sportID := range s.sportIDs {
|
||
s.loadLiveMatches(sportID)
|
||
s.logger.Println("load end")
|
||
}
|
||
}
|
||
}
|
||
}
|