Skip to content

[Bug] Crash após salvar histórico e antes de clearBets reaplica apostas da corrida anterior na próxima corrida #58

Description

@deraldinho

Resumo

O Monster Derby persiste o resultado da corrida e limpa as apostas em duas operações separadas, sem transação e sem associar mdt_bets a um race_id.

No encerramento da corrida, a ordem é:

  1. salvar mdt_history da corrida atual;
  2. limpar mdt_bets;
  3. incrementar _raceNumber apenas em memória.

Se o processo cair depois do INSERT INTO mdt_history e antes de clearBets(), o banco fica com:

  • histórico da corrida N já persistido;
  • apostas da corrida N ainda presentes em mdt_bets.

No restart:

  • loadHistory() encontra N e define _raceNumber = N + 1;
  • loadBets() carrega as apostas antigas sem saber a qual corrida pertenciam;
  • o countdown inicia uma nova corrida N+1 com esses valores já no pool.

Assim, apostas pagas para a corrida anterior passam a contaminar as odds da corrida seguinte.

Evidências no código

Bets não possuem race_id

DerbyTrackManager usa:

private static final String LOAD_BETS = "SELECT * FROM mdt_bets";
private static final String SAVE_BETS = "REPLACE INTO mdt_bets (lane_id, bet) VALUES (?,?)";
private static final String CLEAR_BETS = "UPDATE mdt_bets SET bet = 0";

O estado persistido é apenas:

lane_id -> bet

sem versão/corrida associada.

Startup determina a corrida pelo histórico

private void loadHistory()
{
    ...
    if (_raceNumber <= savedRaceNumber)
        _raceNumber = savedRaceNumber + 1;
}

Depois:

private void loadBets()
{
    ...
    while (rs.next())
        setBetOnLane(rs.getInt("lane_id"), rs.getLong("bet"), false);
}

Não existe verificação de que os bets carregados pertencem a _raceNumber.

Encerramento persiste histórico antes de limpar bets

No case 1115 do countdown:

final HistoryInfo info = getHistoryInfo(_raceNumber);
if (info != null)
{
    info.setFirst(getFirst());
    info.setSecond(getSecond());
    info.setOddRate(_odds.get(getFirst()));

    try (... PreparedStatement ps = con.prepareStatement(SAVE_HISTORY))
    {
        ps.setInt(1, info.getRaceId());
        ...
        ps.execute();
    }
}

clearBets();
...
_raceNumber++;

As operações não estão na mesma transação.

Cenário de reprodução

Suponha corrida 100 em andamento com bets persistidos.

  1. Corrida 100 termina.
  2. SAVE_HISTORY insere race_id = 100 com vencedor/odd.
  3. Antes de clearBets(), o GameServer sofre crash/kill.
  4. Banco contém:
    • mdt_history: corrida 100 finalizada;
    • mdt_bets: valores ainda pertencentes à corrida 100.
  5. Servidor reinicia.
  6. loadHistory() encontra 100 e define:
    _raceNumber = 101
    
  7. loadBets() carrega os bets antigos da corrida 100.
  8. Countdown inicia newRace() para a corrida 101.
  9. Quando calculateOdds() rodar, os valores antigos já compõem o pool da corrida 101.

Jogadores da corrida 101 podem receber odds/payout influenciados por apostas que pertenciam a tickets da corrida 100.

Impacto

Correção proposta

A solução robusta é versionar bets por corrida:

mdt_bets(race_id, lane_id, bet)

E carregar apenas:

WHERE race_id = currentRace

O fechamento deve ser transacional:

  1. persistir resultado da corrida N;
  2. marcar/encerrar bets da corrida N;
  3. avançar estado/corrida;
  4. commit único.

Alternativamente, persistir explicitamente current_race_id junto ao mapa de bets e rejeitar/limpar bets cuja versão não corresponda ao histórico carregado.

Também é recomendável tornar clearBets() e avanço de _raceNumber parte da mesma operação durável.

Critérios de aceite

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions