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 é:
- salvar
mdt_history da corrida atual;
- limpar
mdt_bets;
- 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:
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.
- Corrida 100 termina.
SAVE_HISTORY insere race_id = 100 com vencedor/odd.
- Antes de
clearBets(), o GameServer sofre crash/kill.
- Banco contém:
mdt_history: corrida 100 finalizada;
mdt_bets: valores ainda pertencentes à corrida 100.
- Servidor reinicia.
loadHistory() encontra 100 e define:
loadBets() carrega os bets antigos da corrida 100.
- Countdown inicia
newRace() para a corrida 101.
- 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:
- persistir resultado da corrida N;
- marcar/encerrar bets da corrida N;
- avançar estado/corrida;
- 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
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_betsa umrace_id.No encerramento da corrida, a ordem é:
mdt_historyda corrida atual;mdt_bets;_raceNumberapenas em memória.Se o processo cair depois do
INSERT INTO mdt_historye antes declearBets(), o banco fica com:mdt_bets.No restart:
loadHistory()encontra N e define_raceNumber = N + 1;loadBets()carrega as apostas antigas sem saber a qual corrida pertenciam;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
DerbyTrackManagerusa:O estado persistido é apenas:
sem versão/corrida associada.
Startup determina a corrida pelo histórico
Depois:
Não existe verificação de que os bets carregados pertencem a
_raceNumber.Encerramento persiste histórico antes de limpar bets
No
case 1115do countdown:As operações não estão na mesma transação.
Cenário de reprodução
Suponha corrida 100 em andamento com bets persistidos.
SAVE_HISTORYinsererace_id = 100com vencedor/odd.clearBets(), o GameServer sofre crash/kill.mdt_history: corrida 100 finalizada;mdt_bets: valores ainda pertencentes à corrida 100.loadHistory()encontra 100 e define:loadBets()carrega os bets antigos da corrida 100.newRace()para a corrida 101.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
clearBets()seguinte.Correção proposta
A solução robusta é versionar bets por corrida:
E carregar apenas:
O fechamento deve ser transacional:
Alternativamente, persistir explicitamente
current_race_idjunto 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_raceNumberparte da mesma operação durável.Critérios de aceite
loadBets()carrega somente bets dorace_idcorreto.SAVE_HISTORYeclearBets().