From 00908623593af9103741d42c7500467f2c6b6d90 Mon Sep 17 00:00:00 2001 From: Brad Brown Date: Tue, 21 Jun 2022 18:30:54 -0500 Subject: [PATCH] atomicity fixes --- wordlinator/db/pg.py | 254 +++++++++++++++++++++++-------------------- 1 file changed, 135 insertions(+), 119 deletions(-) diff --git a/wordlinator/db/pg.py b/wordlinator/db/pg.py index 1be8b02..78a9539 100644 --- a/wordlinator/db/pg.py +++ b/wordlinator/db/pg.py @@ -85,140 +85,155 @@ class WordleDb: return list(User.select()) def get_users_by_round(self, round_no=None, round_id=None): - query = ( - User.select(User, Player.user_id, Game.game) - .join(Player, on=(Player.user_id == User.user_id)) - .join(Game, on=(Game.game_id == Player.game_id)) - ) - if round_no: - query = query.filter(Game.game == round_no) - elif round_id: - query = query.filter(Game.game_id == round_id) - return list(query) + with db.atomic(): + query = ( + User.select(User, Player.user_id, Game.game) + .join(Player, on=(Player.user_id == User.user_id)) + .join(Game, on=(Game.game_id == Player.game_id)) + ) + if round_no: + query = query.filter(Game.game == round_no) + elif round_id: + query = query.filter(Game.game_id == round_id) + return list(query) def get_user_id(self, username): - user = self.get_user(username) - return user.twitter_id if user else None + with db.atomic(): + user = self.get_user(username) + return user.twitter_id if user else None def add_user(self, username, user_id, check_twitter=True): - return User.create( - username=username, twitter_id=user_id, check_twitter=check_twitter - ) + with db.atomic(): + return User.create( + username=username, twitter_id=user_id, check_twitter=check_twitter + ) def get_rounds(self): - return list(sorted(Game.select(), key=lambda d: d.start_date)) + with db.atomic(): + return list(sorted(Game.select(), key=lambda d: d.start_date)) def get_or_create_round(self, round_no, start_date=None): - try: - return Game.get(Game.game == round_no) - except peewee.DoesNotExist: - start_date = ( - start_date or wordlinator.utils.WORDLE_GOLF_ROUND_DATES[round_no - 1] - ) - return Game.create(game=round_no, start_date=start_date) + with db.atomic(): + try: + return Game.get(Game.game == round_no) + except peewee.DoesNotExist: + start_date = ( + start_date + or wordlinator.utils.WORDLE_GOLF_ROUND_DATES[round_no - 1] + ) + return Game.create(game=round_no, start_date=start_date) def get_or_create_hole(self, round_no, hole_no): - round = self.get_or_create_round(round_no) + with db.atomic(): + round = self.get_or_create_round(round_no) - try: - return Hole.get(Hole.hole == hole_no, Hole.game_id == round.game_id) - except peewee.DoesNotExist: - return Hole.create(hole=hole_no, game_id=round.game_id) + try: + return Hole.get(Hole.hole == hole_no, Hole.game_id == round.game_id) + except peewee.DoesNotExist: + return Hole.create(hole=hole_no, game_id=round.game_id) def get_holes(self, round_no, ensure_all=False): - round = self.get_or_create_round(round_no) - if ensure_all: - self.create_round_holes(round_no) - return list(Hole.select().filter(game_id=round.game_id)) + with db.atomic(): + round = self.get_or_create_round(round_no) + if ensure_all: + self.create_round_holes(round_no) + return list(Hole.select().filter(game_id=round.game_id)) def create_round_holes(self, round_no): - for hole_no in range(1, 19): - self.get_or_create_hole(round_no, hole_no) + with db.atomic(): + for hole_no in range(1, 19): + self.get_or_create_hole(round_no, hole_no) def get_or_create_player_round(self, user_id, game_id): - try: - return Player.get(Player.user_id == user_id, Player.game_id == game_id) - except peewee.DoesNotExist: - return Player.create(user_id=user_id, game_id=game_id) + with db.atomic(): + try: + return Player.get(Player.user_id == user_id, Player.game_id == game_id) + except peewee.DoesNotExist: + return Player.create(user_id=user_id, game_id=game_id) def add_user_to_round(self, username, round_no): - user = self.get_user(username) - if not user: - raise ValueError(f"No user found with username {username}") - round = self.get_or_create_round(round_no) - return self.get_or_create_player_round(user.user_id, round.game_id) + with db.atomic(): + user = self.get_user(username) + if not user: + raise ValueError(f"No user found with username {username}") + round = self.get_or_create_round(round_no) + return self.get_or_create_player_round(user.user_id, round.game_id) def remove_user_from_round(self, username, round_no): - user = self.get_user(username) - if not user: - raise ValueError(f"No user found with username {username}") - round = self.get_or_create_round(round_no) - try: - player = Player.get(user_id=user.user_id, game_id=round.game_id) - player.delete_instance() - except peewee.DoesNotExist: - return + with db.atomic(): + user = self.get_user(username) + if not user: + raise ValueError(f"No user found with username {username}") + round = self.get_or_create_round(round_no) + try: + player = Player.get(user_id=user.user_id, game_id=round.game_id) + player.delete_instance() + except peewee.DoesNotExist: + return def copy_players_from_round(self, from_round_no, to_round_no): - to_round = self.get_or_create_round(to_round_no) + with db.atomic(): + to_round = self.get_or_create_round(to_round_no) - for user in self.get_users_by_round(from_round_no): - self.get_or_create_player_round(user.user_id, to_round.game_id) + for user in self.get_users_by_round(from_round_no): + self.get_or_create_player_round(user.user_id, to_round.game_id) def add_score(self, username, game, hole, score): - if not score: - return + with db.atomic(): + if not score: + return - user = self.get_user(username) - if not user: - raise ValueError(f'No Such User "{username}"') - hole = self.get_or_create_hole(game, hole) + user = self.get_user(username) + if not user: + raise ValueError(f'No Such User "{username}"') + hole = self.get_or_create_hole(game, hole) - try: - score_obj = Score.get( - Score.user_id == user.user_id, - Score.game_id == hole.game_id, - Score.hole_id == hole.hole_id, - ) - score_obj.score = score - score_obj.save() - return score_obj - except peewee.DoesNotExist: - return Score.create( - score=score, - user_id=user.user_id, - game_id=hole.game_id, - hole_id=hole.hole_id, - ) + try: + score_obj = Score.get( + Score.user_id == user.user_id, + Score.game_id == hole.game_id, + Score.hole_id == hole.hole_id, + ) + score_obj.score = score + score_obj.save() + return score_obj + except peewee.DoesNotExist: + return Score.create( + score=score, + user_id=user.user_id, + game_id=hole.game_id, + hole_id=hole.hole_id, + ) def get_scores(self, round_no=None, round_id=None): - if round_no: - round = self.get_or_create_round(round_no) - elif round_id: - round = Game.get_by_id(round_id) - else: - raise ValueError("Must provide Round Number or Round ID") - res = ( - Score.select( - Score, - Hole.hole, - User.username, - Player.game_id, + with db.atomic(): + if round_no: + round = self.get_or_create_round(round_no) + elif round_id: + round = Game.get_by_id(round_id) + else: + raise ValueError("Must provide Round Number or Round ID") + res = ( + Score.select( + Score, + Hole.hole, + User.username, + Player.game_id, + ) + .join(Player, on=(Score.user_id == Player.user_id)) + .switch(Score) + .join(Hole, on=(Score.hole_id == Hole.hole_id)) + .switch(Score) + .join(User, on=(Score.user_id == User.user_id)) + .filter(Player.game_id == round.game_id) + .filter(Score.game_id == round.game_id) ) - .join(Player, on=(Score.user_id == Player.user_id)) - .switch(Score) - .join(Hole, on=(Score.hole_id == Hole.hole_id)) - .switch(Score) - .join(User, on=(Score.user_id == User.user_id)) - .filter(Player.game_id == round.game_id) - .filter(Score.game_id == round.game_id) - ) - return list(res) + return list(res) if res else [] def bulk_insert_scores(self, scores: typing.List[typing.Dict]): - with db.atomic(): + with db.atomic() as txn: for batch in peewee.chunked(scores, 50): - Score.insert_many(batch).execute() + Score.insert_many(batch).execute(txn) def bulk_update_scores(self, scores: typing.List[Score]): with db.atomic(): @@ -226,24 +241,25 @@ class WordleDb: score.save() def get_users_without_score(self, round_no, hole_no, tweetable=True): - hole = self.get_or_create_hole(round_no, hole_no) - # Find users who *have* played in this round, - # but have no score on the current hole - query_str = """SELECT u.username, player.game_id - FROM user_tbl u - JOIN player ON player.user_id = u.user_id - WHERE ( - player.game_id = {} - ) AND NOT EXISTS ( - SELECT FROM score WHERE score.user_id = u.user_id AND score.hole_id = {} - ) - """.format( - hole.game_id, hole.hole_id - ) + with db.atomic() as txn: + hole = self.get_or_create_hole(round_no, hole_no) + # Find users who *have* played in this round, + # but have no score on the current hole + query_str = """SELECT u.username, player.game_id + FROM user_tbl u + JOIN player ON player.user_id = u.user_id + WHERE ( + player.game_id = {} + ) AND NOT EXISTS ( + SELECT FROM score WHERE score.user_id = u.user_id AND score.hole_id = {} + ) + """.format( + hole.game_id, hole.hole_id + ) - if tweetable: - # Restrict to users who post scores on twitter - query_str += " AND u.check_twitter = true" + if tweetable: + # Restrict to users who post scores on twitter + query_str += " AND u.check_twitter = true" - res = db.execute_sql(query_str) - return [r[0] for r in res] + res = txn.execute_sql(query_str) + return [r[0] for r in res]