[Python] Redis OM persistent data issue

Discussão em 'Python' iniciado por Stack, Setembro 3, 2025.

  1. Stack

    Stack Membro Participativo

    I am using Redis OM (redis-om==0.3.5) for my chess like game where two people join and make moves one by one. The game is CacheGame and player is CacheGamePlayer. The issue is on high concurrency, lets say from 30000 games per day I am seeing following discrepancies for like 90-120 times.

    Game ID XMHFS-NGKd: Turn Discrepancy detected - move_count=18 move_history_length=18. Moves: ['b3e3 599/120 600/120 1', 'b10c8 599/120 595/120 5', 'b1c3 597/120 595/120 2', 'h10g8 597/ ....


    What that means is some times we are some how are getting staled data, likely from following places

    • latest_game = CacheGame.get(game_id)
    • self.game = CacheGame.get(game_id)

    Here’s a rephrased version of your text with smoother flow and clarity:


    We’re running Flask on the backend with eventlet monkey patching, which relies on greenlets. The purpose of using pipeline.watch and multi/execute is to avoid working with stale data. if another process updates the key, it should trigger a WatchError. My concern is that since I’m creating a separate connection to Redis OM, this might be why the data isn’t always up to date. The goal is to ensure that what I store in Redis OM is immediately reflected in memory. Could you verify if this is indeed the root cause, or if something else might be responsible?


    @function_trace(name='apply_move')
    def apply_move(self):
    """
    Apply a move using a Check-and-Set (CAS) pattern to ensure atomicity.

    This function will WATCH the game's Redis key. If the key is modified by another
    process before this transaction completes, a WatchError is raised, and the
    operation is retried with the fresh game data.
    """
    client = CacheGame.Meta.database
    game_id = self.game.game_id

    for attempt in range(MAX_CAS_RETRIES):
    try:
    # Use a pipeline to group WATCH, GET, and the transaction
    with client.pipeline() as pipe:
    game_key = CacheGame.make_primary_key(game_id)
    pipe.watch(game_key)

    latest_game = CacheGame.get(game_id)
    self.game = latest_game
    self.board.fen = latest_game.fen

    self.validate_move()
    self.set_move_result()
    self.apply_move_updates()

    # This will fail and raise WatchError if `game_key` was changed.
    pipe.multi()
    self.game.save(pipeline=pipe)
    result = pipe.execute()

    # Ensure exactly one command succeeded
    if not result or len(result) != 1 or result[0] is not True:
    raise PersistenceError

    # Persisted state is now our current state.
    self.game = CacheGame.get(game_id)
    self.execute_post_move_operations()

    if self.game.state == GameStates.END.value:
    celery.send_task(GameTasks.GAME_END_TASK.value, args=(game_id,))

    return

    except WatchError:
    logger.error(
    f'CATCH AND SET conflict on game {game_id} attempt {attempt + 1}/{MAX_CAS_RETRIES}. Retrying...'
    )
    time.sleep(CAS_RETRY_DELAY_SECS)

    continue

    except (RedisError, PersistenceError) as err:
    logger.error(f'PersistenceError during move on game {game_id}: {err}', exc_info=True)
    self.emit_error_response()

    return


    def apply_move_updates(self):
    current_time = get_current_time_in_ms()
    self.elapsed_secs = self.game.get_elapsed_secs(current_time, self.thinking_seconds)

    if self.game.is_timed:
    self.game.update_player_timers(self.elapsed_secs)

    self.game.fen = self.board.fen
    self.game.server_clock = current_time
    self.game.move_count += 1
    self.game.last_move_timestamp = datetime.now()
    # Expiry time must be updated once fen and server_clock set
    self.game.expiry_time = round(time.time() + self.game.get_game_expiry_secs() + GAME_EXPIRY_GRACE_SECS)

    if not self.game.bot_game:
    self.set_requester_states()

    self.formatted_move = self.get_formatted_move(self.elapsed_secs)
    self.pre_move = self.game.pre_move
    self.game.pre_move = None
    self.set_move_history()

    if self.game.move_count > len(self.game.move_history):
    logger.error('Discrepancy detected: move count exceeds recorded move history length.')
    logger.error(
    f'Game ID {self.game.game_id}: Discrepancy detected - move_count={self.game.move_count} exceeds '
    f'move_history_length={len(self.game.move_history)}. Moves: {self.game.move_history}. '
    f'Snapshot: server_clock={self.game.server_clock}, expiry_time={self.game.expiry_time}, '
    f'current_turn={self.game.turn_now}, fen={self.game.fen}')

    end_reason = self.move_result.get('end_reason')

    if end_reason:
    game_result = (
    f'{self.move_side.label}_{GameResult.WON.label}'
    if end_reason in MATE_REASONS
    else GameResult.DRAW.label
    )
    self.game.end_game(end_reason, game_result=game_result, save=False)

    Continue reading...

Compartilhe esta Página