Files
onexbet/daemon/daemon.go
2026-07-21 03:55:57 +03:00

271 lines
7.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}
}
}