/// <summary>
        /// Removes expired tickets and notifications
        /// </summary>
        private void CleanUpExpiredItems()
        {
            // tickets
            var tickets = entity
                          .DeserializeTickets <TMatchmakerTicket>();

            tickets.RemoveAll(t => t.PlayerId == null); // should not happen, but
            tickets.RemoveAll(
                t => t.NotPolledForSeconds > TicketExpirySeconds
                );
            entity.SerializeTickets(tickets);

            // notifications
            entity.Notifications.RemoveAll(
                n => (n.createdAt - DateTime.UtcNow)
                .TotalSeconds > NotificationExpirySeconds
                );

            // matches
            CleanUpMatches();
        }
        /// <summary>
        /// Player wants to join this matchmaker and start waiting
        /// </summary>
        /// <param name="ticket">Ticket of the player</param>
        /// <exception cref="ArgumentException">
        /// Ticket owner and facet caller differ
        /// </exception>
        public void JoinMatchmaker(TMatchmakerTicket ticket)
        {
            // null ticket owner gets set to the caller
            if (ticket.PlayerId == null)
            {
                ticket.PlayerId = Caller.EntityId;
            }

            // ticket owner has to match the caller
            if (ticket.PlayerId != Caller.EntityId)
            {
                throw new ArgumentException(
                          "Ticket belongs to a different player " +
                          "than the one registering it.",
                          nameof(ticket)
                          );
            }

            PrepareNewTicket(ticket);

            entity = GetEntity();

            DB.RetryOnConflict(() => {
                entity.Refresh();
                var tickets = entity
                              .DeserializeTickets <TMatchmakerTicket>();

                // if already waiting, perform a re-insert
                tickets.RemoveAll(t => t.PlayerId == ticket.PlayerId);

                // if to be notified, remove from notifications
                entity.Notifications.RemoveAll(
                    n => n.playerId == Caller.EntityId
                    );

                // add ticket into the queue
                ticket.InsertedNow();
                tickets.Add(ticket);

                entity.SerializeTickets(
                    tickets
                    );
                entity.SaveCarefully();
            });
        }
        /// <summary>
        /// Player polls for new status on his/her matching
        /// </summary>
        /// <param name="leave">Player wants to leave the matchmaker</param>
        /// <returns>Null if not matched yet, match entity otherwise</returns>
        /// <exception cref="UnknownPlayerPollingException">
        /// When the matchmaker has no clue why is this player polling
        /// </exception>
        public TMatchEntity PollMatchmaker(bool leave)
        {
            entity = GetEntity();

            TMatchEntity returnedValue = null;

            // first perform cleanup
            DB.RetryOnConflict(() => {
                entity.Refresh();
                CleanUpExpiredItems();
                entity.SaveCarefully();
            });

            DB.RetryOnConflict(() => {
                entity.Refresh();

                // player not waiting -> throw
                // unless there's a notification for this player
                if (entity.Notifications.All(
                        n => n.playerId != Caller.EntityId
                        ))
                {
                    var tickets = entity
                                  .DeserializeTickets <TMatchmakerTicket>();

                    if (tickets.All(t => t.PlayerId != Caller.EntityId))
                    {
                        throw new UnknownPlayerPollingException(
                            "Polling, but not waiting in ticket queue, " +
                            "nor having a notification prepared."
                            );
                    }
                }

                // update poll time for this ticket
                {
                    var tickets = entity
                                  .DeserializeTickets <TMatchmakerTicket>();
                    var ticket = tickets.FirstOrDefault(
                        t => t.PlayerId == Caller.EntityId
                        );
                    ticket?.PolledNow();
                    entity.SerializeTickets(
                        tickets
                        );
                }

                // attempt to create matches
                CallCreateMatches();

                // find notification to return
                var notification = entity.Notifications
                                   .FirstOrDefault(n => n.playerId == Caller.EntityId);
                if (notification != null)
                {
                    returnedValue = DB.Find <TMatchEntity>(notification.matchId);

                    entity.Notifications.RemoveAll(
                        n => n.playerId == Caller.EntityId
                        );
                }

                // stop waiting no match, but leaving requested
                if (leave && returnedValue == null)
                {
                    var tickets = entity
                                  .DeserializeTickets <TMatchmakerTicket>();
                    tickets.RemoveAll(t => t.PlayerId == Caller.EntityId);
                    entity.SerializeTickets(
                        tickets
                        );
                }

                entity.SaveCarefully();
            });

            return(returnedValue);
        }