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