// subscribe subscribes to the topic for this game // Returns a channel of Messages that can be used to receive messages // Make sure to send it a new redis.Conn. It will handle closing it // when finished. func subscribe(ctx context.Context, con redis.Conn, g *Game) (<-chan *message, error) { lc := "Subscribe" c := make(chan *message) psc := redis.PubSubConn{con} logger.Info(ctx, lc, "Subscribing to topic '%v'", g.ID) err := psc.Subscribe(g.ID) if err != nil { logger.Info(ctx, lc, "Error Subscribing. %v", err) close(c) return c, err } go func(c chan<- *message) { defer con.Close() defer close(c) for { switch v := psc.Receive().(type) { case redis.Message: msg := new(message) err := msg.unmarshalGob(v.Data) if err != nil { logger.Error(ctx, lc, "Could not decode message. Closing channel. %v, %v", v, err) return } logger.Info(ctx, lc, "Received Message. Sending %#v to channel.", msg) c <- msg // special case for LostMessage, since we know to close at this point. if msg.Type == lostMessage { logger.Info(ctx, lc, "Lost Message, Closing Subscribe Pipeline.") return } case error: logger.Error(ctx, lc, "Error processing messages. Closing channel. %v", err) return default: logger.Info(ctx, lc, "Received unknown message. Ignored: %#v", v) } } }(c) return c, nil }
// findGame finds a game in the list of open games. If one doesn't exist, creates a new gameid // returns a new Game and if it's a new game or not. func findGame(ctx context.Context, con redis.Conn) (*Game, bool, error) { lc := "FindGame" // do we have an open game? gameID, err := redis.String(con.Do("RPOP", openGames)) // ignore nil errors, since that is expected if err != nil && err != redis.ErrNil { logger.Error(ctx, lc, "Error finding open game: %v", err) return new(Game), false, err } // is this a brand new game? isNew := (gameID == "") if isNew { logger.Info(ctx, lc, "Could not find open game, creating one... ") u, err := uuid.NewV4() if err != nil { return nil, false, err } gameID = u.String() } return NewGame(gameID), isNew, nil }
// ensureSubscribers Make sure n number of Game subscriptions at this point. // Blocks until we have two people. Times out on too many retries. func ensureSubscribers(ctx context.Context, con redis.Conn, g *Game, n int) error { lc := "EnsureSubscribers" for i := 0; i <= 5; i++ { res, err := con.Do("PUBSUB", "NUMSUB", g.ID) if err != nil { logger.Error(ctx, lc, "Error getting number of subscriptions: %v", err) return err } vals, err := redis.Values(res, err) if err != nil { logger.Error(ctx, lc, "Error converting to values: %v", err) return err } if l := len(vals); l != 2 { err := fmt.Errorf("Should only be two items in the result. Weird. %#v. %v.", vals, l) logger.Error(ctx, lc, err.Error()) return err } count, ok := vals[1].(int64) if !ok { val := vals[1] err := fmt.Errorf("Second value should be an integer. %T %#v", val, val) logger.Error(ctx, lc, err.Error()) return err } logger.Info(ctx, lc, "Found %v subscriptions for Game. Require %v", count, n) if int(count) == n { return nil } logger.Info(ctx, lc, "Could not find enough subscriptions, retrying...") time.Sleep(100 * time.Millisecond) } err := errors.New("Timeout attempting to ensure subscriber count of " + strconv.Itoa(n)) logger.Error(ctx, lc, err.Error()) return err }
// receivePress Receives a press. Returns an error if there is an issue, and publishes it // to redis as well. func receivePressRequest(stream SimonSays_GameServer) (*Request_Press, error) { lc := "receivePressRequest" ctx := stream.Context() res, err := receiveRequest(stream) if err != nil { logger.Error(ctx, lc, "Error recieving from gRPC: %v", err) return nil, err } press, ok := res.Event.(*Request_Press) if !ok { err := errors.New("Recieved a request other than a Press") logger.Error(ctx, lc, "Error: %v. %v", err, res) return nil, err } return press, nil }
// publish publishes a message to the game's topic. func publish(ctx context.Context, con redis.Conn, g *Game, msg message) error { lc := "Publish" logger.Info(ctx, lc, "Sending message: %#v, to topic: '%v'", msg, g.ID) data, err := msg.marshalGob() if err != nil { logger.Error(ctx, lc, "Error encoding message. %#v, %v", msg, err) return err } _, err = con.Do("PUBLISH", g.ID, data) if err != nil { logger.Error(ctx, lc, "Error publishing message. %#v, %v", msg, err) } return err }
// sendResponse Sends a request. func sendResponse(stream SimonSays_GameServer, r *Response) error { lc := "Response" ctx := stream.Context() logger.Info(ctx, lc, "Sending response: %v", r) err := stream.Send(r) if err != nil { logger.Error(ctx, lc, "Error sending: %v", err) } return err }
// handle Processing pub/sub events and does things with them // Think "controller". func handle(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error { lc := "Handler" ctx := stream.Context() logger.Info(ctx, lc, "Handling Message: %#v", msg) fn, ok := handlers[msg.Type] if !ok { logger.Error(ctx, lc, "Could not find a handler for this event. %#v", msg) return handlerNotFoundError("msg.Type") } return fn(con, game, player, stream, msg) }
// receiveRequest receives a request. func receiveRequest(stream SimonSays_GameServer) (*Request, error) { lc := "Request" ctx := stream.Context() req, err := stream.Recv() if err != nil { logger.Error(ctx, lc, "Error recieving: %v", err) return nil, err } logger.Info(ctx, lc, "Received: %v", req) return req, nil }
// handleEndOfTurn handles if it is the end of the turn, and if the player has lost (bool). func handleEndOfTurn(stream SimonSays_GameServer, con redis.Conn, game *Game, player *Request_Player) (bool, error) { lc := "handleEndOfTurn" ctx := stream.Context() // if not my turn, exit early. if game.isMyTurn() { return false, nil } if game.match() { b, err := game.encodePresses() if err != nil { logger.Error(ctx, lc, "Error encoding presses: %#v, %v", game, err) return false, err } msg := message{Type: stopTurnMessage, Player: player.Id, Data: b} if err := publish(ctx, con, game, msg); err != nil { logger.Error(ctx, lc, "error publishing StopTurnMessage %#v, %v", msg, err) return false, err } return false, nil } // if there is no match, you did something wrong. otherwise, my friend, you have lost the game. msg := message{Type: lostMessage, Player: player.Id} if err := publish(ctx, con, game, msg); err != nil { logger.Error(ctx, lc, "error publishing LostMessage %#v, %v", msg, err) return false, err } // and we are done taking input - end of game! logger.Info(ctx, lc, "We are done taking input. Returning that we have lost. %#v", game) return true, nil }
// lightUpHandler handles LIGHTUP events, letting everyone know to lightup // their colours. func lightUpHandler(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error { lc := "lightUpHandler" ctx := stream.Context() c := new(Color) buf := bytes.NewBuffer(msg.Data) err := gob.NewDecoder(buf).Decode(c) if err != nil { logger.Error(ctx, lc, "Could not convert colour. %#v. %v", msg, err) return err } logger.Info(ctx, lc, "Sending stream response to Light Up %v", c) return sendResponse(stream, &Response{Event: &Response_Lightup{Lightup: *c}}) }
// beginHandler Streams BEGIN to client once we are good to go. func beginHandler(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error { lc := "beginHandler" ctx := stream.Context() res := &Response{Event: &Response_Turn{Turn: Response_BEGIN}} err := sendResponse(stream, res) if err != nil { logger.Error(ctx, lc, "Error sending BEGIN event. %v", err) return err } // if not the player that BEGAN (so first player to join), then START your turn if msg.Player == player.Id { logger.Info(ctx, lc, "Publishing end turn %v", stopTurnMessage) return publish(ctx, con, game, message{Player: player.Id, Type: stopTurnMessage, Data: msg.Data}) } logger.Info(ctx, lc, "Not doing anything with Begin. It's not my job.") return nil }
// handleColorPress handles one color being pressed. // If it's the player turn it modifies the given game and sends a lightUpMessage to Redis. // This function is thread safe. func handleColorPress(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer) (bool, error) { lc := "handleColorPress" ctx := stream.Context() press, err := receivePressRequest(stream) if err != nil { logger.Error(ctx, lc, "Press Error. Sending to err channel, and shutting down %v", err) return true, err } logger.Info(ctx, lc, "Press Received: %v", press) //lock the game for this entire block, since we are doing lots of things with //it, and this will prevent any concurrency issues. game.mu.Lock() defer game.mu.Unlock() // only accept input when it is my turn! if !game.isMyTurn() { logger.Info(ctx, lc, "Not my turn. Ignored press.") return false, nil } err = game.pressColor(press.Press) if err == ErrColorPressedOutOfTurn { logger.Info(ctx, lc, "Colour pressed out of turn. Ignored.") } else if err != nil { return true, err } err = sendLightupEvent(press, stream, con, game) if err != nil { return true, err } // When you reach the point that the game has turned. return handleEndOfTurn(stream, con, game, player) }
// stopTurnHandler My turn has finished, so, tell the other player // to START_TURN, and me to END_TURN. func stopTurnHandler(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error { lc := "stopTurnHandler" ctx := stream.Context() // if I'm the player that sent out the message, let the client know if player.Id == msg.Player { return sendResponse(stream, &Response{Event: &Response_Turn{Turn: Response_STOP_TURN}}) } // otherwise, it's time for the other player to start buf := bytes.NewBuffer(msg.Data) c := []Color{} err := gob.NewDecoder(buf).Decode(&c) if err != nil { logger.Error(ctx, lc, "Error decoding message colors. %#v. %v", msg, err) return err } logger.Info(ctx, lc, "Starting turn with colors: %v", c) game.StartTurn(c) return sendResponse(stream, &Response{Event: &Response_Turn{Turn: Response_START_TURN}}) }
// Game function is an implementation of the gRPC Game Service. // When connected, this is the main functionality of running a // Game for the connected player. func (s *SimonSays) Game(stream SimonSays_GameServer) error { ctx := stream.Context() defer logger.Clear(ctx) lc := "Game" // first let's get the player req, err := receiveRequest(stream) if err != nil { return err } player := req.GetJoin() if player == nil { logger.Error(ctx, lc, "Player was nil on initial join request. %v", req) return errors.New("Player was nil on initial join request.") } logger.Set(ctx, "Player", player.Id) logger.Info(ctx, lc, "Player %#v is attempting to join.", player) // find what game to join con := s.pool.Get() defer con.Close() game, isNew, err := findGame(ctx, con) if err != nil { return err } logger.Set(ctx, "Game", game.ID) logger.Info(ctx, lc, "Connecting to game %v. New?: %v", game.ID, isNew) logger.Info(ctx, lc, "Start to receive PubSub messages") // make sure that you always unjoin, if something happens to go wrong. defer func() { con := s.pool.Get() err := closeOpenGame(ctx, con, game) if err != nil { logger.Error(ctx, lc, "Error attempting to close game. %v", err) } err = con.Close() if err != nil { logger.Error(ctx, lc, "Error closing close open game connection. %v", err) } }() // make sure that at the end, you always unsubscribe. defer func() { con := s.pool.Get() err := redis.PubSubConn{con}.Unsubscribe(game.ID) if err != nil { logger.Error(ctx, lc, "Error unsubscribing from Game Topic %v, %v", game.ID, err) } err = con.Close() if err != nil { logger.Error(ctx, lc, "Error closing unsubscribe connection. %v", err) } }() msgs, err := subscribe(ctx, s.pool.Get(), game) if err != nil { return err } if err := connectGame(ctx, con, game, player, isNew); err != nil { return err } // subscribe to incoming key events, and get back a channel of errors. perrs := recvPress(s.pool.Get(), game, player, stream) for { select { // process incoming messages from RedisPubSub, and send messages. case msg := <-msgs: if msg == nil { logger.Error(ctx, lc, "Message Channel has closed. Exiting.") return nil } logger.Info(ctx, lc, "Handling incoming messsage...") err := handle(con, game, player, stream, msg) if err != nil { // if we are EOF, then simply exit. if err == io.EOF { logger.Info(ctx, lc, "[Game] EOF. Closing connection.") return nil } return err } // check to see if there are any issues with press errors. case err := <-perrs: // remember, a closed channel, will return a nil err. if err != nil { logger.Error(ctx, lc, "There was a press error. %v", err) return err } } } }