diff --git a/dist/app/Http/Controllers/Api/Matchmaking/MatchmakingController.php b/dist/app/Http/Controllers/Api/Matchmaking/MatchmakingController.php index c3c2236..be173f8 100644 --- a/dist/app/Http/Controllers/Api/Matchmaking/MatchmakingController.php +++ b/dist/app/Http/Controllers/Api/Matchmaking/MatchmakingController.php @@ -22,14 +22,19 @@ use App\Models\User\User; use Auth; use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Laravel\Prompts\Table; class MatchmakingController extends Controller { const TRY_CREATE_MATCH_INTERVAL_SECONDS = 5; + const QUEUE_LOCK = 'queuedPlayers'; + public function getRegions() { return ["EU"]; @@ -44,6 +49,28 @@ public function queue(QueueRequest $request) return json_encode($this->addPlayerToQueue($request)); } + public function cancelQueue() + { + $user = Auth::user(); + + $lock = Cache::lock(static::QUEUE_LOCK, 10); + + try { + $lock->block(20 ,function () use (&$user) { + // Delete the player from the Queue + QueuedPlayer::where('user_id', '=', $user->id)->delete(); + // And also from any game they are maczhed for. + DB::table('game_user')->where('user_id', '=', $user->id)->delete(); + }); + } catch (LockTimeoutException $e) { + Log::channel('matchmaking')->emergency('Queue Cancel: Could not acquire Lock for canceling user '.$user->id.'('.$user->last_known_username.')'); + } finally { + $lock?->release(); + } + + return response('', 204); + } + public function matchInfo(string $matchId) { $foundGame = Game::find($matchId); @@ -235,23 +262,25 @@ protected function checkQueueStatus(QueueRequest $request): QueueResponse protected function addPlayerToQueue(QueueRequest $request) { - $user = Auth::user(); - if($user->activeGames()->exists()) - return $this->checkQueueStatus($request); + Cache::lock(static::QUEUE_LOCK, 10)->block(20, function () use ($request) { + $user = Auth::user(); + if($user->activeGames()->exists()) + return; - $queued = QueuedPlayer::firstOrCreate(['user_id' => $user->id]); - $queued->leader()->disassociate(); - $queued->side = $request->side; - $queued->user()->associate($user->id); - $queued->save(); + $queued = QueuedPlayer::firstOrCreate(['user_id' => $user->id]); + $queued->leader()->disassociate(); + $queued->side = $request->side; + $queued->user()->associate($user->id); + $queued->save(); - foreach ($request->additionalUserIds as $additionalUserId) { - $follower = QueuedPlayer::firstOrCreate(['user_id' => $additionalUserId]); - $follower->side = $request->side; - $follower->user()->associate($additionalUserId); - $follower->save(); - $queued->followingUsers()->save($follower); - } + foreach ($request->additionalUserIds as $additionalUserId) { + $follower = QueuedPlayer::firstOrCreate(['user_id' => $additionalUserId]); + $follower->side = $request->side; + $follower->user()->associate($additionalUserId); + $follower->save(); + $queued->followingUsers()->save($follower); + } + }); return $this->checkQueueStatus($request); } @@ -262,7 +291,11 @@ protected function processQueue(): void if(!(Cache::get('tryCreateMatch', 0) < time() - static::TRY_CREATE_MATCH_INTERVAL_SECONDS)) return; - Cache::set('tryCreateMatch', time()); + $lock = Cache::lock(static::QUEUE_LOCK, 20); + + // If we cannot acquire the lock, do nothing + if(!$lock->get()) + return; // Select all queued Players/party leaders, descending by party size $players = QueuedPlayer::withCount('followingUsers') @@ -288,22 +321,27 @@ protected function processQueue(): void $playerCount = $this->getTotalPlayersCount($players); $availableMatchConfigs = MatchConfiguration::getAvailableMatchConfigs($playerCount->runners, $playerCount->hunters); - if($availableMatchConfigs->isEmpty()) + if($availableMatchConfigs->isEmpty()) { + $lock->release(); return; + } $selectedConfig = MatchConfiguration::selectRandomConfigByWeight($availableMatchConfigs); // Should never happen, but just to be careful - if($selectedConfig === null) + if($selectedConfig === null) { + $lock->release(); return; + } $hunterGroupsSet = $this->determineMatchingPlayers($hunters, $selectedConfig->hunters); $runnerGroupsSet = $this->determineMatchingPlayers($runners, $selectedConfig->runners); // if we cannot create a match with our current player groups, stop - if($runnerGroupsSet === false || $hunterGroupsSet === false) + if($runnerGroupsSet === false || $hunterGroupsSet === false) { + $lock->release(); return; - + } rsort($runnerGroupsSet, SORT_NUMERIC); rsort($hunterGroupsSet, SORT_NUMERIC); @@ -330,6 +368,10 @@ protected function processQueue(): void } $newGame->determineHost(); + + // Set cached timeat the end of processing and release lock + Cache::set('tryCreateMatch', time()); + $lock->release(); } protected function tryFillOpenGames(Collection|array &$hunters, Collection|array &$runners) diff --git a/dist/app/Models/User/User.php b/dist/app/Models/User/User.php index 6cd0f4e..7cc55ae 100644 --- a/dist/app/Models/User/User.php +++ b/dist/app/Models/User/User.php @@ -4,11 +4,14 @@ use App\Enums\Game\Matchmaking\MatchStatus; use App\Models\Game\Matchmaking\Game; +use Cache; +use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as AuthUser; +use Illuminate\Support\Facades\Log; use Spatie\Permission\Traits\HasRoles; /** @@ -19,6 +22,8 @@ class User extends AuthUser { use HasUuids, HasRoles; + const PLAYER_DATA_LOCK = 'playerData'; + protected $fillable = [ 'steam_id', 'source', @@ -32,7 +37,19 @@ public function ban(): HasOne public function playerData(): PlayerData|Model { - // Added Shared lock to stop a race condition between initOrGetGroups and GetQuitterState + // Shared Lock alone didn't prevent sometimes duplicate playerData entries. + // Hopefully this lock will work. + $lock = Cache::lock(static::PLAYER_DATA_LOCK, 5); + + try { + $lock->block(5); + $playerData = $this->hasOne(PlayerData::class)->sharedLock()->firstOrCreate(); + $lock->release(); + return $playerData; + } catch (LockTimeoutException $e) { + Log::channel('daily')->emergency('Could not Acquire Player data lock, this should not happen.'); + } + return $this->hasOne(PlayerData::class)->sharedLock()->firstOrCreate(); } diff --git a/dist/routes/deathgardenApi.php b/dist/routes/deathgardenApi.php index a18b738..397c588 100644 --- a/dist/routes/deathgardenApi.php +++ b/dist/routes/deathgardenApi.php @@ -66,6 +66,7 @@ Route::post('queue', [MatchmakingController::class, 'queue']); + Route::post('queue/cancel', [MatchmakingController::class, 'cancelQueue']); Route::get('match/{matchId}', [MatchmakingController::class, 'matchInfo']); Route::post('match/{matchId}/register', [MatchmakingController::class, 'register']); Route::put('match/{matchId}/Close', [MatchmakingController::class, 'close']);