Implement Redis for storing rooms

This commit is contained in:
jvyden 2022-05-15 12:04:10 -04:00
parent e998e59607
commit e12a798fd5
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
14 changed files with 194 additions and 67 deletions

View file

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Development Database" type="docker-deploy" factoryName="docker-compose.yml" <configuration default="false" name="Development Databases" type="docker-deploy" factoryName="docker-compose.yml"
server-name="Docker"> server-name="Docker">
<deployment type="docker-compose.yml"> <deployment type="docker-compose.yml">
<settings> <settings>
@ -7,6 +7,7 @@
<option name="sourceFilePath" value="docker-compose.yml"/> <option name="sourceFilePath" value="docker-compose.yml"/>
</settings> </settings>
</deployment> </deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isPublishEnabled="true"/>
<method v="2"/> <method v="2"/>
</configuration> </configuration>
</component> </component>

View file

@ -120,7 +120,7 @@ public class LoginController : ControllerBase
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
// Create a new room on LBP2/3/Vita // Create a new room on LBP2/3/Vita
if (token.GameVersion != GameVersion.LittleBigPlanet1) RoomHelper.CreateRoom(user, token.GameVersion, token.Platform); if (token.GameVersion != GameVersion.LittleBigPlanet1) RoomHelper.CreateRoom(user.UserId, token.GameVersion, token.Platform);
return this.Ok return this.Ok
( (

View file

@ -74,10 +74,10 @@ public class MatchController : ControllerBase
if (matchData is UpdateMyPlayerData playerData) if (matchData is UpdateMyPlayerData playerData)
{ {
MatchHelper.SetUserLocation(user.UserId, gameToken.UserLocation); MatchHelper.SetUserLocation(user.UserId, gameToken.UserLocation);
Room? room = RoomHelper.FindRoomByUser(user, gameToken.GameVersion, gameToken.Platform, true); Room? room = RoomHelper.FindRoomByUser(user.UserId, gameToken.GameVersion, gameToken.Platform, true);
if (playerData.RoomState != null) if (playerData.RoomState != null)
if (room != null && Equals(room.Host, user)) if (room != null && Equals(room.HostId, user.UserId))
room.State = (RoomState)playerData.RoomState; room.State = (RoomState)playerData.RoomState;
} }
@ -101,12 +101,12 @@ public class MatchController : ControllerBase
if (matchData is CreateRoom createRoom && MatchHelper.UserLocations.Count >= 1) if (matchData is CreateRoom createRoom && MatchHelper.UserLocations.Count >= 1)
{ {
List<User> users = new(); List<int> users = new();
foreach (string playerUsername in createRoom.Players) foreach (string playerUsername in createRoom.Players)
{ {
User? player = await this.database.Users.FirstOrDefaultAsync(u => u.Username == playerUsername); User? player = await this.database.Users.FirstOrDefaultAsync(u => u.Username == playerUsername);
// ReSharper disable once ConditionIsAlwaysTrueOrFalse // ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (player != null) users.Add(player); if (player != null) users.Add(player.UserId);
else return this.BadRequest(); else return this.BadRequest();
} }
@ -116,7 +116,7 @@ public class MatchController : ControllerBase
if (matchData is UpdatePlayersInRoom updatePlayersInRoom) if (matchData is UpdatePlayersInRoom updatePlayersInRoom)
{ {
Room? room = RoomHelper.Rooms.FirstOrDefault(r => r.Host == user); Room? room = RoomHelper.Rooms.FirstOrDefault(r => r.HostId == user.UserId);
if (room != null) if (room != null)
{ {
@ -129,7 +129,7 @@ public class MatchController : ControllerBase
else return this.BadRequest(); else return this.BadRequest();
} }
room.Players = users; room.PlayerIds = users.Select(u => u.UserId).ToList();
RoomHelper.CleanupRooms(null, room); RoomHelper.CleanupRooms(null, room);
} }
} }

View file

@ -1,3 +1,4 @@
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -22,12 +23,12 @@ public class RoomVisualizerController : ControllerBase
#if !DEBUG #if !DEBUG
return this.NotFound(); return this.NotFound();
#else #else
List<User> users = await this.database.Users.OrderByDescending(_ => EF.Functions.Random()).Take(2).ToListAsync(); List<int> users = await this.database.Users.OrderByDescending(_ => EF.Functions.Random()).Take(2).Select(u => u.UserId).ToListAsync();
RoomHelper.CreateRoom(users, GameVersion.LittleBigPlanet2, Platform.PS3); RoomHelper.CreateRoom(users, GameVersion.LittleBigPlanet2, Platform.PS3);
foreach (User user in users) foreach (int user in users)
{ {
MatchHelper.SetUserLocation(user.UserId, "127.0.0.1"); MatchHelper.SetUserLocation(user, "127.0.0.1");
} }
return this.Redirect("/debug/roomVisualizer"); return this.Redirect("/debug/roomVisualizer");
#endif #endif
@ -39,7 +40,7 @@ public class RoomVisualizerController : ControllerBase
#if !DEBUG #if !DEBUG
return this.NotFound(); return this.NotFound();
#else #else
RoomHelper.Rooms.RemoveAll(_ => true); RoomHelper.Rooms.DeleteAll();
return this.Redirect("/debug/roomVisualizer"); return this.Redirect("/debug/roomVisualizer");
#endif #endif
} }

View file

@ -1,4 +1,5 @@
@page "/debug/roomVisualizer" @page "/debug/roomVisualizer"
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Types @using LBPUnion.ProjectLighthouse.Types
@using LBPUnion.ProjectLighthouse.Types.Match @using LBPUnion.ProjectLighthouse.Types.Match
@ -35,7 +36,7 @@
<meta http-equiv="refresh" content="@refreshSeconds"> <meta http-equiv="refresh" content="@refreshSeconds">
</noscript> </noscript>
<p>@RoomHelper.Rooms.Count rooms</p> <p>@RoomHelper.Rooms.Count() rooms</p>
<a href="/debug/roomVisualizer/createFakeRoom"> <a href="/debug/roomVisualizer/createFakeRoom">
<div class="ui blue button">Create Fake Room</div> <div class="ui blue button">Create Fake Room</div>
@ -63,7 +64,7 @@
@foreach (Room room in RoomHelper.Rooms) @foreach (Room room in RoomHelper.Rooms)
{ {
bool userInRoom = room.Players.Select(p => p.Username).Contains(Model.User?.Username); bool userInRoom = room.PlayerIds.Contains(Model.User?.UserId ?? -1);
string color = userInRoom ? "green" : "blue"; string color = userInRoom ? "green" : "blue";
<div class="ui @color inverted segment"> <div class="ui @color inverted segment">
<h3>Room @room.RoomId</h3> <h3>Room @room.RoomId</h3>
@ -73,9 +74,9 @@
<b>You are currently in this room.</b> <b>You are currently in this room.</b>
</p> </p>
} }
<p>@room.Players.Count players, state is @room.State, version is @room.RoomVersion.ToPrettyString()on paltform @room.RoomPlatform</p> <p>@room.PlayerIds.Count players, state is @room.State, version is @room.RoomVersion.ToPrettyString()on paltform @room.RoomPlatform</p>
<p>Slot type: @room.Slot.SlotType, slot id: @room.Slot.SlotId</p> <p>Slot type: @room.Slot.SlotType, slot id: @room.Slot.SlotId</p>
@foreach (User player in room.Players) @foreach (User player in room.GetPlayers(Model.Database))
{ {
<div class="ui segment">@player.Username</div> <div class="ui segment">@player.Username</div>
} }

View file

@ -0,0 +1,27 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Redis.OM.Searching;
namespace LBPUnion.ProjectLighthouse.Extensions;
[SuppressMessage("ReSharper", "LoopCanBePartlyConvertedToQuery")]
public static class IRedisCollectionExtensions
{
public static void DeleteAll<T>(this IRedisCollection<T> collection, Func<T, bool> predicate)
{
foreach (T item in collection)
{
if (!predicate.Invoke(item)) continue;
collection.DeleteSync(item);
}
}
public static void DeleteAll<T>(this IRedisCollection<T> collection)
{
foreach (T item in collection)
{
collection.DeleteSync(item);
}
}
}

View file

@ -0,0 +1,25 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Match;
namespace LBPUnion.ProjectLighthouse.Extensions;
public static class RoomExtensions
{
public static List<User> GetPlayers(this Room room, Database database)
{
List<User> players = new();
foreach (int playerId in room.PlayerIds)
{
User? player = database.Users.FirstOrDefault(p => p.UserId == playerId);
Debug.Assert(player != null);
players.Add(player);
}
return players;
}
}

View file

@ -3,17 +3,20 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Match; using LBPUnion.ProjectLighthouse.Types.Match;
using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Profiles;
using Redis.OM;
using Redis.OM.Searching;
namespace LBPUnion.ProjectLighthouse.Helpers; namespace LBPUnion.ProjectLighthouse.Helpers;
public class RoomHelper public class RoomHelper
{ {
public static readonly List<Room> Rooms = new(); public static readonly IRedisCollection<Room> Rooms = Redis.GetRooms();
public static readonly RoomSlot PodSlot = new() public static readonly RoomSlot PodSlot = new()
{ {
@ -49,14 +52,9 @@ public class RoomHelper
return null; return null;
} }
bool anyRoomsLookingForPlayers; IEnumerable<Room> rooms = Rooms;
List<Room> rooms;
lock(Rooms) rooms = rooms.OrderBy(r => r.IsLookingForPlayers);
{
anyRoomsLookingForPlayers = Rooms.Any(r => r.IsLookingForPlayers);
rooms = anyRoomsLookingForPlayers ? Rooms.Where(r => anyRoomsLookingForPlayers && r.IsLookingForPlayers).ToList() : Rooms;
}
rooms = rooms.Where(r => r.RoomVersion == roomVersion).ToList(); rooms = rooms.Where(r => r.RoomVersion == roomVersion).ToList();
if (platform != null) rooms = rooms.Where(r => r.RoomPlatform == platform).ToList(); if (platform != null) rooms = rooms.Where(r => r.RoomPlatform == platform).ToList();
@ -72,24 +70,24 @@ public class RoomHelper
// Don't attempt to dive into the current room the player is in. // Don't attempt to dive into the current room the player is in.
if (user != null) if (user != null)
{ {
rooms = rooms.Where(r => !r.Players.Contains(user)).ToList(); rooms = rooms.Where(r => !r.PlayerIds.Contains(user.UserId)).ToList();
} }
foreach (Room room in rooms) foreach (Room room in rooms)
// Look for rooms looking for players before moving on to rooms that are idle. // Look for rooms looking for players before moving on to rooms that are idle.
{ {
if (user != null && MatchHelper.DidUserRecentlyDiveInWith(user.UserId, room.Host.UserId)) continue; if (user != null && MatchHelper.DidUserRecentlyDiveInWith(user.UserId, room.HostId)) continue;
Dictionary<int, string> relevantUserLocations = new(); Dictionary<int, string> relevantUserLocations = new();
// Determine if all players in a room have UserLocations stored, also store the relevant userlocations while we're at it // Determine if all players in a room have UserLocations stored, also store the relevant userlocations while we're at it
bool allPlayersHaveLocations = room.Players.All bool allPlayersHaveLocations = room.PlayerIds.All
( (
p => p =>
{ {
bool gotValue = MatchHelper.UserLocations.TryGetValue(p.UserId, out string? value); bool gotValue = MatchHelper.UserLocations.TryGetValue(p, out string? value);
if (gotValue && value != null) relevantUserLocations.Add(p.UserId, value); if (gotValue && value != null) relevantUserLocations.Add(p, value);
return gotValue; return gotValue;
} }
); );
@ -104,7 +102,7 @@ public class RoomHelper
response.Players = new List<Player>(); response.Players = new List<Player>();
response.Locations = new List<string>(); response.Locations = new List<string>();
foreach (User player in room.Players) foreach (User player in room.GetPlayers(new Database()))
{ {
response.Players.Add response.Players.Add
( (
@ -147,74 +145,82 @@ public class RoomHelper
return null; return null;
} }
public static Room CreateRoom(User user, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null) public static Room CreateRoom(int userId, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null)
=> CreateRoom => CreateRoom
( (
new List<User> new List<int>
{ {
user, userId,
}, },
roomVersion, roomVersion,
roomPlatform, roomPlatform,
slot slot
); );
public static Room CreateRoom(List<User> users, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null) public static Room CreateRoom(List<int> users, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null)
{ {
Room room = new() Room room = new()
{ {
RoomId = RoomIdIncrement, RoomId = RoomIdIncrement,
Players = users, PlayerIds = users,
State = RoomState.Idle, State = RoomState.Idle,
Slot = slot ?? PodSlot, Slot = slot ?? PodSlot,
RoomVersion = roomVersion, RoomVersion = roomVersion,
RoomPlatform = roomPlatform, RoomPlatform = roomPlatform,
}; };
CleanupRooms(room.Host, room); CleanupRooms(room.HostId, room);
lock(Rooms) Rooms.Add(room); lock(Rooms) Rooms.Insert(room);
Logger.LogInfo($"Created room (id: {room.RoomId}) for host {room.Host.Username} (id: {room.Host.UserId})", LogArea.Match); Logger.LogInfo($"Created room (id: {room.RoomId}) for host {room.HostId}", LogArea.Match);
return room; return room;
} }
public static Room? FindRoomByUser(User user, GameVersion roomVersion, Platform roomPlatform, bool createIfDoesNotExist = false) public static Room? FindRoomByUser(int userId, GameVersion roomVersion, Platform roomPlatform, bool createIfDoesNotExist = false)
{ {
lock(Rooms) lock(Rooms)
foreach (Room room in Rooms.Where(room => room.Players.Any(player => user == player))) foreach (Room room in Rooms.Where(room => room.PlayerIds.Any(player => userId == player)))
return room; return room;
return createIfDoesNotExist ? CreateRoom(user, roomVersion, roomPlatform) : null; return createIfDoesNotExist ? CreateRoom(userId, roomVersion, roomPlatform) : null;
} }
public static Room? FindRoomByUserId(int userId) public static Room? FindRoomByUserId(int userId)
{ {
lock(Rooms) lock(Rooms)
foreach (Room room in Rooms.Where(room => room.Players.Any(player => player.UserId == userId))) foreach (Room room in Rooms)
return room; {
if (room.PlayerIds.Any(p => p == userId))
{
return room;
}
}
return null; return null;
} }
[SuppressMessage("ReSharper", "InvertIf")] [SuppressMessage("ReSharper", "InvertIf")]
public static void CleanupRooms(User? host = null, Room? newRoom = null) public static void CleanupRooms(int? hostId = null, Room? newRoom = null, Database? database = null)
{ {
// return;
lock(Rooms) lock(Rooms)
{ {
int roomCountBeforeCleanup = Rooms.Count; int roomCountBeforeCleanup = Rooms.Count();
// Remove offline players from rooms // Remove offline players from rooms
foreach (Room room in Rooms) foreach (Room room in Rooms)
{ {
// do not shorten, this prevents collection modified errors List<User> players = room.GetPlayers(database ?? new Database());
List<User> playersToRemove = room.Players.Where(player => player.Status.StatusType == StatusType.Offline).ToList();
foreach (User user in playersToRemove) room.Players.Remove(user); List<int> playersToRemove = players.Where(player => player.Status.StatusType == StatusType.Offline).Select(player => player.UserId).ToList();
foreach (int player in playersToRemove) room.PlayerIds.Remove(player);
} }
// Delete old rooms based on host // Delete old rooms based on host
if (host != null) if (hostId != null)
try try
{ {
Rooms.RemoveAll(r => r.Host == host); Rooms.DeleteAll(r => r.HostId == hostId);
} }
catch catch
{ {
@ -227,13 +233,13 @@ public class RoomHelper
{ {
if (room == newRoom) continue; if (room == newRoom) continue;
foreach (User newRoomPlayer in newRoom.Players) room.Players.RemoveAll(p => p == newRoomPlayer); foreach (int newRoomPlayer in newRoom.PlayerIds) room.PlayerIds.RemoveAll(p => p == newRoomPlayer);
} }
Rooms.RemoveAll(r => r.Players.Count == 0); // Remove empty rooms Rooms.DeleteAll(r => r.PlayerIds.Count == 0); // Remove empty rooms
Rooms.RemoveAll(r => r.Players.Count > 4); // Remove obviously bogus rooms Rooms.DeleteAll(r => r.PlayerIds.Count > 4); // Remove obviously bogus rooms
int roomCountAfterCleanup = Rooms.Count; int roomCountAfterCleanup = Rooms.Count();
if (roomCountBeforeCleanup != roomCountAfterCleanup) if (roomCountBeforeCleanup != roomCountAfterCleanup)
{ {

View file

@ -18,4 +18,5 @@ public enum LogArea
Photos, Photos,
Resources, Resources,
Logger, Logger,
Redis,
} }

View file

@ -0,0 +1,41 @@
using System;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types.Match;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Redis.OM;
using Redis.OM.Contracts;
using Redis.OM.Searching;
namespace LBPUnion.ProjectLighthouse;
public static class Redis
{
private static readonly RedisConnectionProvider provider;
static Redis()
{
provider = new RedisConnectionProvider(ServerConfiguration.Instance.RedisConnectionString);
}
private static bool initialized = false;
public static async Task Initialize()
{
if (initialized) throw new InvalidOperationException("Redis has already been initialized.");
IRedisConnection connection = getConnection();
await connection.CreateIndexAsync(typeof(Room));
initialized = true;
Logger.LogSuccess("Initialized Redis.", LogArea.Redis);
}
private static IRedisConnection getConnection()
{
Logger.LogDebug("Getting a redis connection", LogArea.Redis);
return provider.Connection;
}
public static IRedisCollection<Room> GetRooms() => provider.RedisCollection<Room>();
}

View file

@ -76,6 +76,9 @@ public static class StartupTasks
Logger.LogInfo("Starting room cleanup thread...", LogArea.Startup); Logger.LogInfo("Starting room cleanup thread...", LogArea.Startup);
RoomHelper.StartCleanupThread(); RoomHelper.StartCleanupThread();
Logger.LogInfo("Initializing Redis...", LogArea.Startup);
Redis.Initialize().Wait();
stopwatch.Stop(); stopwatch.Stop();
Logger.LogSuccess($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LogArea.Startup); Logger.LogSuccess($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LogArea.Startup);
} }

View file

@ -1,36 +1,47 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using LBPUnion.ProjectLighthouse.Types.Levels; using Redis.OM.Modeling;
namespace LBPUnion.ProjectLighthouse.Types.Match; namespace LBPUnion.ProjectLighthouse.Types.Match;
[Document(StorageType = StorageType.Json)]
public class Room public class Room
{ {
[JsonIgnore] private int roomId;
public List<User> Players { get; set; }
public int RoomId { get; set; } public int RoomId {
get => this.roomId;
set {
this.RedisId = value.ToString();
this.roomId = value;
}
}
[JsonIgnore] [RedisIdField]
public string RedisId { get; set; }
[Indexed]
public List<int> PlayerIds { get; set; }
[Indexed]
public GameVersion RoomVersion { get; set; } public GameVersion RoomVersion { get; set; }
[JsonIgnore] [Indexed]
public Platform RoomPlatform { get; set; } public Platform RoomPlatform { get; set; }
[Indexed]
public RoomSlot Slot { get; set; } public RoomSlot Slot { get; set; }
[Indexed]
public RoomState State { get; set; } public RoomState State { get; set; }
[JsonIgnore] [JsonIgnore]
public bool IsInPod => this.Slot.SlotType == SlotType.Pod; [Indexed]
[JsonIgnore]
public bool IsLookingForPlayers => this.State == RoomState.PlayingLevel || this.State == RoomState.DivingInWaiting; public bool IsLookingForPlayers => this.State == RoomState.PlayingLevel || this.State == RoomState.DivingInWaiting;
[JsonIgnore] [JsonIgnore]
public User Host => this.Players[0]; public int HostId => this.PlayerIds[0];
public int PlayerCount => this.Players.Count;
#nullable enable #nullable enable
public override bool Equals(object? obj) public override bool Equals(object? obj)

View file

@ -22,7 +22,7 @@ public class ServerConfiguration
// You can use an ObsoleteAttribute instead. Make sure you set it to error, though. // You can use an ObsoleteAttribute instead. Make sure you set it to error, though.
// //
// Thanks for listening~ // Thanks for listening~
public const int CurrentConfigVersion = 3; public const int CurrentConfigVersion = 4;
#region Meta #region Meta
@ -170,6 +170,7 @@ public class ServerConfiguration
public string ApiListenUrl { get; set; } = "http://localhost:10062"; public string ApiListenUrl { get; set; } = "http://localhost:10062";
public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse"; public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
public string RedisConnectionString { get; set; } = "";
public string ExternalUrl { get; set; } = "http://localhost:10060"; public string ExternalUrl { get; set; } = "http://localhost:10060";
public bool ConfigReloading { get; set; } public bool ConfigReloading { get; set; }
public string EulaText { get; set; } = ""; public string EulaText { get; set; } = "";

View file

@ -14,6 +14,15 @@ services:
- '3306' # Expose port to localhost:3306 - '3306' # Expose port to localhost:3306
volumes: volumes:
- lighthouse-db:/var/lib/mysql - lighthouse-db:/var/lib/mysql
lighthouse-redis:
image: redis/redis-stack
ports:
- '6379:6379'
- '8001:8001'
expose:
- '6379'
- '8001'
volumes: volumes:
lighthouse-db: lighthouse-db: