Skip to content

Commit

Permalink
Battle action tracker
Browse files Browse the repository at this point in the history
  • Loading branch information
Atralupus committed Feb 27, 2025
1 parent 267cbaa commit 953b892
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 85 deletions.
32 changes: 32 additions & 0 deletions ArenaService.Shared/Jwt/BattleTokenValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,36 @@ public bool ValidateBattleToken(string token, int battleId)
return false;
}
}
public bool TryValidateBattleToken(string token, out JwtPayload payload)
{
try
{
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "planetarium arena service",

ValidateAudience = true,
ValidAudience = "NineChronicles headless",

ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,

ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(_rsa)
};

var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(token, validationParameters, out SecurityToken validated);
var validatedToken = (JwtSecurityToken)validated;

payload = validatedToken.Payload;
return true;
}
catch (Exception)
{
payload = null;

Check warning on line 83 in ArenaService.Shared/Jwt/BattleTokenValidator.cs

View workflow job for this annotation

GitHub Actions / Test (ArenaService.Tests)

Cannot convert null literal to non-nullable reference type.
return false;
}
}
}
12 changes: 12 additions & 0 deletions ArenaService.Shared/Repositories/AvailableOpponentRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Task<AvailableOpponent> UpdateAvailableOpponent(
Action<AvailableOpponent> updateFields
);
Task<int?> GetSuccessBattleId(int availableOpponentId);
Task<bool> TrySetSuccessBattleId(int availableOpponentId, int battleId);
}

public class AvailableOpponentRepository : IAvailableOpponentRepository
Expand Down Expand Up @@ -185,4 +186,15 @@ Action<AvailableOpponent> updateFields
.Select(ao => ao.SuccessBattleId)
.SingleOrDefaultAsync();
}

public async Task<bool> TrySetSuccessBattleId(int availableOpponentId, int battleId)
{
var rowsAffected = await _context.AvailableOpponents
.Where(ao => ao.Id == availableOpponentId && ao.SuccessBattleId == null)
.ExecuteUpdateAsync(setters => setters
.SetProperty(ao => ao.SuccessBattleId, battleId)
.SetProperty(ao => ao.UpdatedAt, DateTime.UtcNow));

return rowsAffected > 0;
}
}
57 changes: 23 additions & 34 deletions ArenaService.Shared/Repositories/TicketRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -644,44 +644,33 @@ public async Task<bool> DeductBattleTicket(
bool isVictory
)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var roundTicketUpdateCount = await _context.BattleTicketStatusesPerRound
.Where(status => status.Id == roundTicketStatusId)
.ExecuteUpdateAsync(status => status
.SetProperty(x => x.RemainingCount, x => x.RemainingCount - 1)
.SetProperty(x => x.UsedCount, x => x.UsedCount + 1)
.SetProperty(x => x.WinCount, x => x.WinCount + (isVictory ? 1 : 0))
.SetProperty(x => x.LoseCount, x => x.LoseCount + (isVictory ? 0 : 1))
.SetProperty(x => x.UpdatedAt, DateTime.UtcNow)
);

if (roundTicketUpdateCount < 1)
{
return false;
}

var seasonTicketUpdateCount = await _context.BattleTicketStatusesPerSeason
.Where(status => status.Id == seasonTicketStatusId)
.ExecuteUpdateAsync(status => status
.SetProperty(x => x.UsedCount, x => x.UsedCount + 1)
.SetProperty(x => x.UpdatedAt, DateTime.UtcNow)
);

if (seasonTicketUpdateCount > 0)
{
await transaction.CommitAsync();
return true;
}
var roundTicketUpdateCount = await _context.BattleTicketStatusesPerRound
.Where(status => status.Id == roundTicketStatusId)
.ExecuteUpdateAsync(status => status
.SetProperty(x => x.RemainingCount, x => x.RemainingCount - 1)
.SetProperty(x => x.UsedCount, x => x.UsedCount + 1)
.SetProperty(x => x.WinCount, x => x.WinCount + (isVictory ? 1 : 0))
.SetProperty(x => x.LoseCount, x => x.LoseCount + (isVictory ? 0 : 1))
.SetProperty(x => x.UpdatedAt, DateTime.UtcNow)
);

await transaction.RollbackAsync();
if (roundTicketUpdateCount < 1)
{
return false;
}
catch

var seasonTicketUpdateCount = await _context.BattleTicketStatusesPerSeason
.Where(status => status.Id == seasonTicketStatusId)
.ExecuteUpdateAsync(status => status
.SetProperty(x => x.UsedCount, x => x.UsedCount + 1)
.SetProperty(x => x.UpdatedAt, DateTime.UtcNow)
);

if (seasonTicketUpdateCount > 0)
{
await transaction.RollbackAsync();
throw;
return true;
}

return false;
}
}
42 changes: 21 additions & 21 deletions ArenaService/Controllers/BattleController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,27 +164,27 @@ public async Task<IActionResult> RequestBattle(int battleId, [FromBody] BattleRe
{
var avatarAddress = HttpContext.User.RequireAvatarAddress();

var battle = await _battleRepo.GetBattleAsync(battleId);

if (battle is null)
{
return NotFound($"Battle log with ID {battleId} not found.");
}

if (battle.AvatarAddress != avatarAddress)
{
return StatusCode(StatusCodes.Status403Forbidden);
}

await _battleRepo.UpdateBattle(
battle,
b =>
{
b.TxId = request.TxId;
}
);

_jobClient.Enqueue<BattleProcessor>(processor => processor.ProcessAsync(battleId));
// var battle = await _battleRepo.GetBattleAsync(battleId);

// if (battle is null)
// {
// return NotFound($"Battle log with ID {battleId} not found.");
// }

// if (battle.AvatarAddress != avatarAddress)
// {
// return StatusCode(StatusCodes.Status403Forbidden);
// }

// await _battleRepo.UpdateBattle(
// battle,
// b =>
// {
// b.TxId = request.TxId;
// }
// );

// _jobClient.Enqueue<BattleProcessor>(processor => processor.ProcessAsync(battleId));

return Ok();
}
Expand Down
15 changes: 15 additions & 0 deletions ArenaService/HeadlessClient/GetTxs.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
query GetTxs($startingBlockIndex: Long!, $limit: Long!, $actionType: String!, $txStatusFilter: [TxStatus!]) {
transaction {
ncTransactions(
startingBlockIndex: $startingBlockIndex
limit: $limit
actionType: $actionType
txStatusFilter: $txStatusFilter
) {
actions {
raw
}
id
}
}
}
39 changes: 9 additions & 30 deletions ArenaService/Processor/BattleProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using ArenaService.Options;
using ArenaService.Services;
using ArenaService.Shared.Constants;
using ArenaService.Shared.Data;
using ArenaService.Shared.Extensions;
using ArenaService.Shared.Jwt;
using ArenaService.Shared.Models;
Expand Down Expand Up @@ -39,7 +40,7 @@ public class BattleProcessor
private readonly ITicketRepository _ticketRepo;
private readonly ITxTrackingService _txTrackingService;
private readonly BattleTokenValidator _battleTokenValidator;
private readonly DbContext _dbContext;
private readonly ArenaDbContext _dbContext;

public BattleProcessor(
ILogger<BattleProcessor> logger,
Expand All @@ -53,7 +54,7 @@ public BattleProcessor(
ITxTrackingService txTrackingService,
IParticipantRepository participantRepo,
IOptions<OpsConfigOptions> options,
DbContext dbContext
ArenaDbContext dbContext
)
{
_logger = logger;
Expand Down Expand Up @@ -354,15 +355,17 @@ BattleResultState battleResult
using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted);
try
{
// 첫 번째 검증
var initialSuccessBattleId = await _availableOpponentRepo.GetSuccessBattleId(battle.AvailableOpponent.Id);
if (initialSuccessBattleId is not null)
var updated = await _availableOpponentRepo.TrySetSuccessBattleId(
battle.AvailableOpponent.Id,
battle.Id
);

if (!updated)
{
await transaction.RollbackAsync();
return "Already have success battle id";
}

// 기존 로직 수행
var deductResult = await _ticketRepo.DeductBattleTicket(
battleTicketStatusPerRound.Id,
battleTicketStatusPerSeason.Id,
Expand Down Expand Up @@ -426,13 +429,6 @@ await _ticketRepo.AddBattleTicketUsageLog(
battle.Id
);

await _availableOpponentRepo.UpdateAvailableOpponent(
battle.AvailableOpponent,
ao =>
{
ao.SuccessBattleId = battle.Id;
}
);
await _userRepo.UpdateUserAsync(
battle.Participant.User,
u =>
Expand All @@ -448,23 +444,6 @@ await _userRepo.UpdateUserAsync(
await _medalRepo.AddOrUpdateMedal(battle.SeasonId, battle.AvatarAddress);
}

// 커밋 직전 최종 검증
var finalSuccessBattleId = await _availableOpponentRepo.GetSuccessBattleId(battle.AvailableOpponent.Id);
if (finalSuccessBattleId is not null)
{
// 다른 워커가 먼저 battle id를 설정했으므로 현재 트랜잭션의 모든 변경사항을 롤백
await transaction.RollbackAsync();

// 현재 배틀 상태를 INVALID로 업데이트
await _battleRepo.UpdateBattle(
battle,
b => b.BattleStatus = BattleStatus.INVALID_BATTLE
);

return "Another worker has already processed this battle";
}

// 최종 검증을 통과했으므로 트랜잭션 커밋
await transaction.CommitAsync();
return "success";
}
Expand Down
4 changes: 4 additions & 0 deletions ArenaService/Setup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ headlessOptions.Value.JwtSecretKey is not null
.Get<OpsConfigOptions>();

services.AddSingleton(new BattleTokenGenerator(opsConfig!.JwtSecretKey));
services.AddSingleton(new BattleTokenValidator(opsConfig!.JwtPublicKey));
services
.AddSingleton<CacheBlockTipWorker>()
.AddHostedService(provider => provider.GetRequiredService<CacheBlockTipWorker>());
Expand All @@ -197,6 +198,9 @@ headlessOptions.Value.JwtSecretKey is not null
services
.AddSingleton<AllClanRankingWorker>()
.AddHostedService(provider => provider.GetRequiredService<AllClanRankingWorker>());
services
.AddSingleton<BattleTxTracker>()
.AddHostedService(provider => provider.GetRequiredService<BattleTxTracker>());

services.AddHangfireServer();
services.AddHealthChecks();
Expand Down
Loading

0 comments on commit 953b892

Please sign in to comment.