Skip to content

Commit

Permalink
Some caches need to be updated immediately when a player identity is …
Browse files Browse the repository at this point in the history
…linked or unlinked #627
  • Loading branch information
sussexrick committed Sep 24, 2024
1 parent 61cec2b commit b20b7dc
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 35 deletions.
21 changes: 19 additions & 2 deletions Stoolball.Data.Abstractions/IPlayerCacheInvalidator.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
using Stoolball.Statistics;
using System.Collections.Generic;
using Stoolball.Matches;
using Stoolball.Statistics;
using Stoolball.Teams;

namespace Stoolball.Data.Abstractions
{
public interface IPlayerCacheInvalidator
{
void InvalidateCacheForPlayer(Player cacheable);
/// <summary>
/// Invalidate the cache for a single player, including their key statistics (but not all statistics).
/// </summary>
/// <param name="player">The player to clear the cache for.</param>
void InvalidateCacheForPlayer(Player player);

/// <summary>
/// Invalidate the combined cache of player identities for all of the teams playing in a match.
/// </summary>
/// <param name="teamsInMatch">The teams playing in the match.</param>
void InvalidateCacheForTeams(IEnumerable<TeamInMatch> teamsInMatch);

/// <summary>
/// Invalidate the individual cache of player identities for each supplied team.
/// </summary>
/// <param name="teams">The teams for which individual caches should be cleared.</param>
void InvalidateCacheForTeams(params Team[] teams);
}
}
35 changes: 25 additions & 10 deletions Stoolball.Data.MemoryCache/PlayerCacheInvalidator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Stoolball.Data.Abstractions;
using Stoolball.Matches;
using Stoolball.Routing;
using Stoolball.Statistics;
using Stoolball.Teams;
Expand All @@ -18,35 +20,48 @@ public PlayerCacheInvalidator(IReadThroughCache readThroughCache, IRouteNormalis
_routeNormaliser = routeNormaliser ?? throw new ArgumentNullException(nameof(routeNormaliser));
}

public void InvalidateCacheForPlayer(Player cacheable)
/// <inheritdoc/>
public void InvalidateCacheForPlayer(Player player)
{
if (cacheable is null)
if (player is null)
{
throw new ArgumentNullException(nameof(cacheable));
throw new ArgumentNullException(nameof(player));
}

if (string.IsNullOrEmpty(cacheable.PlayerRoute))
if (string.IsNullOrEmpty(player.PlayerRoute))
{
throw new ArgumentException($"{nameof(cacheable.PlayerRoute)} cannot be null or empty string");
throw new ArgumentException($"{nameof(player.PlayerRoute)} cannot be null or empty string");
}

var normalisedRoute = _routeNormaliser.NormaliseRouteToEntity(cacheable.PlayerRoute, "players");
var normalisedRoute = _routeNormaliser.NormaliseRouteToEntity(player.PlayerRoute, "players");
_readThroughCache.InvalidateCache(nameof(IPlayerDataSource) + nameof(IPlayerDataSource.ReadPlayerByRoute) + normalisedRoute);
if (cacheable.MemberKey.HasValue)
if (player.MemberKey.HasValue)
{
_readThroughCache.InvalidateCache(nameof(IPlayerDataSource) + nameof(IPlayerDataSource.ReadPlayerByMemberKey) + cacheable.MemberKey);
_readThroughCache.InvalidateCache(nameof(IPlayerDataSource) + nameof(IPlayerDataSource.ReadPlayerByMemberKey) + player.MemberKey);
}
_readThroughCache.InvalidateCache(nameof(IPlayerSummaryStatisticsDataSource) + nameof(IPlayerSummaryStatisticsDataSource.ReadBattingStatistics) + normalisedRoute);
_readThroughCache.InvalidateCache(nameof(IPlayerSummaryStatisticsDataSource) + nameof(IPlayerSummaryStatisticsDataSource.ReadBowlingStatistics) + normalisedRoute);
_readThroughCache.InvalidateCache(nameof(IPlayerSummaryStatisticsDataSource) + nameof(IPlayerSummaryStatisticsDataSource.ReadFieldingStatistics) + normalisedRoute);
}

/// <inheritdoc/>
public void InvalidateCacheForTeams(IEnumerable<TeamInMatch> teamsInMatch)
{
if (teamsInMatch is null) { throw new ArgumentNullException(nameof(teamsInMatch)); }

var teamIds = string.Join("--", teamsInMatch.Select(tim => tim.Team?.TeamId).OfType<Guid>().Distinct().OrderBy(x => x.ToString()));
_readThroughCache.InvalidateCache(nameof(IPlayerDataSource) + nameof(IPlayerDataSource.ReadPlayerIdentities) + "ForTeams" + teamIds);
}

/// <inheritdoc/>
public void InvalidateCacheForTeams(params Team[] teams)
{
if (teams == null) { throw new ArgumentNullException(nameof(teams)); }

var teamIds = string.Join("--", teams.Where(x => x?.TeamId != null).Select(x => x.TeamId!.Value).OrderBy(x => x.ToString()));
_readThroughCache.InvalidateCache(nameof(IPlayerDataSource) + nameof(IPlayerDataSource.ReadPlayerIdentities) + "ForTeams" + teamIds);
foreach (var teamId in teams.Select(t => t.TeamId).OfType<Guid>().Distinct())
{
_readThroughCache.InvalidateCache(nameof(IPlayerDataSource) + nameof(IPlayerDataSource.ReadPlayerIdentities) + "ForTeams" + teamId);
}
}
}
}
9 changes: 6 additions & 3 deletions Stoolball.Data.MemoryCache/ReadThroughCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public ReadThroughCache(IMemoryCache memoryCache)
public void InvalidateCache(string key)
{
var cancellationKey = CancellationKey(key);
if (_memoryCache.TryGetValue<CancellationTokenSource>(cancellationKey, out var tokenSource))
if (_memoryCache.TryGetValue<CancellationTokenSource>(cancellationKey, out var tokenSource) && tokenSource is not null)
{
tokenSource.Cancel();
_memoryCache.Remove(cancellationKey);
Expand All @@ -42,14 +42,17 @@ public async Task<TResult> ReadThroughCacheAsync<TResult>(Func<Task<TResult>> ca
_memoryCache.Set(cancellationKey, tokenSource, cancellationCacheExpiry);
}

if (!_memoryCache.TryGetValue<TResult>(dependentKey, out var data))
if (!_memoryCache.TryGetValue<TResult>(dependentKey, out var data) || data is null)
{
data = await cacheThis();
var dataCacheExpiry = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = absoluteCacheExpiration
};
dataCacheExpiry.AddExpirationToken(new CancellationChangeToken(tokenSource.Token));
if (tokenSource is not null)
{
dataCacheExpiry.AddExpirationToken(new CancellationChangeToken(tokenSource.Token));
}
_memoryCache.Set(dependentKey, data, dataCacheExpiry);

// Update expiry of the CancellationSource so it hangs around long enough to expire the newly-cached data
Expand Down
Binary file not shown.
Binary file not shown.
8 changes: 4 additions & 4 deletions Stoolball.Data.SqlServer/SqlServerPlayerDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,18 @@ public async Task<List<PlayerIdentity>> ReadPlayerIdentities(PlayerFilter? filte
{
using (var connection = _databaseConnectionFactory.CreateDatabaseConnection())
{
// Get PlayerIdentity data from the original tables rather than PlayerInMatchStatistics because the original tables
// will be updated when a player identity is renamed, and we need to see the change immediately.
// Get PlayerIdentity and PlayerId data from the original tables rather than PlayerInMatchStatistics because the original tables
// will be updated when a player identity is renamed, or linked or unlinked from a player, and we need to see the change immediately.
// Updates to PlayerInMatchStatistics are done asynchronously and the data will not be updated by the time this is called again.

const string PROBABILITY_CALCULATION = "COUNT(DISTINCT MatchId)*10-DATEDIFF(DAY,MAX(MatchStartTime),GETDATE())";
var sql = $@"SELECT stats.PlayerIdentityId, pi.PlayerIdentityName, pi.RouteSegment, {PROBABILITY_CALCULATION} AS Probability,
COUNT(DISTINCT MatchId) AS TotalMatches, MIN(MatchStartTime) AS FirstPlayed, MAX(MatchStartTime) AS LastPlayed,
stats.PlayerId, stats.PlayerRoute, stats.TeamId, stats.TeamName
pi.PlayerId, stats.PlayerRoute, stats.TeamId, stats.TeamName
FROM {Views.PlayerIdentity} pi INNER JOIN {Tables.PlayerInMatchStatistics} AS stats ON pi.PlayerIdentityId = stats.PlayerIdentityId
<<JOIN>>
<<WHERE>>
GROUP BY stats.PlayerId, stats.PlayerRoute, stats.PlayerIdentityId, pi.PlayerIdentityName, pi.RouteSegment, stats.TeamId, stats.TeamName
GROUP BY pi.PlayerId, stats.PlayerRoute, stats.PlayerIdentityId, pi.PlayerIdentityName, pi.RouteSegment, stats.TeamId, stats.TeamName
ORDER BY stats.TeamId ASC, {PROBABILITY_CALCULATION} DESC, pi.PlayerIdentityName ASC";

var (query, parameters) = BuildQuery(filter, sql);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ public async Task<ActionResult> UpdateMatch([Bind("MatchInnings", "PlayerInnings
if (model.Authorization.CurrentMemberIsAuthorized[AuthorizedAction.EditMatchResult] && ModelState.IsValid)
{
var currentMember = await _memberManager.GetCurrentMemberAsync();
await _matchRepository.UpdateBattingScorecard(model.Match, model.CurrentInnings.MatchInnings.MatchInningsId!.Value, currentMember.Key, currentMember.Name).ConfigureAwait(false);
_playerCacheClearer.InvalidateCacheForTeams(model.CurrentInnings.MatchInnings.BattingTeam!.Team!, model.CurrentInnings.MatchInnings.BowlingTeam!.Team!);
await _matchRepository.UpdateBattingScorecard(model.Match, model.CurrentInnings.MatchInnings.MatchInningsId!.Value, currentMember!.Key, currentMember.Name!).ConfigureAwait(false);
_playerCacheClearer.InvalidateCacheForTeams(model.Match.Teams);

// redirect to the bowling scorecard for this innings
return Redirect($"{model.Match.MatchRoute}/edit/innings/{model.InningsOrderInMatch!.Value}/bowling");
Expand Down
12 changes: 6 additions & 6 deletions Stoolball.Web/Matches/EditBowlingScorecardSurfaceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public async Task<ActionResult> UpdateMatch([Bind("MatchInnings", "OversBowledSe
Bowler = new PlayerIdentity
{
PlayerIdentityName = x.BowledBy?.Trim(),
Team = model.CurrentInnings.MatchInnings.BowlingTeam.Team
Team = model.CurrentInnings.MatchInnings.BowlingTeam!.Team!
},
OverNumber = index + 1,
BallsBowled = x.BallsBowled,
Expand All @@ -134,8 +134,8 @@ public async Task<ActionResult> UpdateMatch([Bind("MatchInnings", "OversBowledSe
if (model.Authorization.CurrentMemberIsAuthorized[AuthorizedAction.EditMatchResult] && ModelState.IsValid)
{
var currentMember = await _memberManager.GetCurrentMemberAsync();
await _matchRepository.UpdateBowlingScorecard(model.Match, model.CurrentInnings.MatchInnings.MatchInningsId!.Value, currentMember.Key, currentMember.Name).ConfigureAwait(false);
_playerCacheClearer.InvalidateCacheForTeams(model.CurrentInnings.MatchInnings.BowlingTeam.Team);
await _matchRepository.UpdateBowlingScorecard(model.Match, model.CurrentInnings.MatchInnings.MatchInningsId!.Value, currentMember!.Key, currentMember.Name!).ConfigureAwait(false);
_playerCacheClearer.InvalidateCacheForTeams(model.CurrentInnings.MatchInnings.BowlingTeam!.Team!);

// redirect to the next innings or close of play
if (model.InningsOrderInMatch!.Value < model.Match.MatchInnings.Count)
Expand All @@ -158,14 +158,14 @@ public async Task<ActionResult> UpdateMatch([Bind("MatchInnings", "OversBowledSe
if (model.Match.Season != null)
{
model.Breadcrumbs.Add(new Breadcrumb { Name = Constants.Pages.Competitions, Url = new Uri(Constants.Pages.CompetitionsUrl, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.Season.Competition.CompetitionName, Url = new Uri(model.Match.Season.Competition.CompetitionRoute, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.Season.SeasonName(), Url = new Uri(model.Match.Season.SeasonRoute, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.Season!.Competition!.CompetitionName, Url = new Uri(model.Match.Season.Competition.CompetitionRoute!, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.Season.SeasonName(), Url = new Uri(model.Match.Season.SeasonRoute!, UriKind.Relative) });
}
else
{
model.Breadcrumbs.Add(new Breadcrumb { Name = Constants.Pages.Matches, Url = new Uri(Constants.Pages.MatchesUrl, UriKind.Relative) });
}
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.MatchName, Url = new Uri(model.Match.MatchRoute, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.MatchName, Url = new Uri(model.Match.MatchRoute!, UriKind.Relative) });

return View("EditBowlingScorecard", model);
}
Expand Down
12 changes: 6 additions & 6 deletions Stoolball.Web/Matches/EditCloseOfPlaySurfaceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ public async Task<ActionResult> UpdateMatch([Bind(Prefix = "FormData")] EditClos
if (model.Match.MatchResultType.HasValue && (int)model.Match.MatchResultType.Value == -1) { model.Match.MatchResultType = null; }

var currentMember = await _memberManager.GetCurrentMemberAsync();
var updatedMatch = await _matchRepository.UpdateCloseOfPlay(model.Match, currentMember.Key, currentMember.Name).ConfigureAwait(false);
var updatedMatch = await _matchRepository.UpdateCloseOfPlay(model.Match, currentMember!.Key, currentMember.Name!).ConfigureAwait(false);
await _matchListingCacheClearer.InvalidateCacheForMatch(beforeUpdate, updatedMatch).ConfigureAwait(false);
_playerCacheClearer.InvalidateCacheForTeams(model.Match.Teams.Select(x => x.Team).OfType<Team>().ToArray());
_playerCacheClearer.InvalidateCacheForTeams(model.Match.Teams);

return Redirect(updatedMatch.MatchRoute);
return Redirect(updatedMatch.MatchRoute!);
}

model.Match.MatchName = beforeUpdate.MatchName;
Expand All @@ -111,14 +111,14 @@ public async Task<ActionResult> UpdateMatch([Bind(Prefix = "FormData")] EditClos
if (model.Match.Season != null)
{
model.Breadcrumbs.Add(new Breadcrumb { Name = Constants.Pages.Competitions, Url = new Uri(Constants.Pages.CompetitionsUrl, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.Season.Competition.CompetitionName, Url = new Uri(model.Match.Season.Competition.CompetitionRoute, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.Season.SeasonName(), Url = new Uri(model.Match.Season.SeasonRoute, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.Season.Competition!.CompetitionName, Url = new Uri(model.Match.Season.Competition.CompetitionRoute!, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.Season.SeasonName(), Url = new Uri(model.Match.Season.SeasonRoute!, UriKind.Relative) });
}
else
{
model.Breadcrumbs.Add(new Breadcrumb { Name = Constants.Pages.Matches, Url = new Uri(Constants.Pages.MatchesUrl, UriKind.Relative) });
}
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.MatchName, Url = new Uri(model.Match.MatchRoute, UriKind.Relative) });
model.Breadcrumbs.Add(new Breadcrumb { Name = model.Match.MatchName, Url = new Uri(model.Match.MatchRoute!, UriKind.Relative) });

return View("EditCloseOfPlay", model);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,12 @@ public async Task<IActionResult> UpdateLinkedPlayers(LinkedPlayersFormData formD
await _playerRepository.UnlinkPlayerIdentity(identity.PlayerIdentityId!.Value, currentMember!.Key, currentMember.Name!);
}

if (identitiesToUnlink.Any())
if (identitiesToLink.Any() || identitiesToUnlink.Any())
{
_playerCacheClearer.InvalidateCacheForPlayer(model.Player);

var teams = model.Player.PlayerIdentities.Select(pi => pi.Team).OfType<Team>().ToArray();
_playerCacheClearer.InvalidateCacheForTeams(teams);
}

var redirectToUrl = model.ContextIdentity.Team.TeamRoute + "/edit/players";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task<IActionResult> RenamePlayerIdentity([Bind(Prefix = "FormData")
Team = model.PlayerIdentity.Team
};

var result = await _playerRepository.UpdatePlayerIdentity(playerIdentity, currentMember.Key, currentMember.UserName).ConfigureAwait(false);
var result = await _playerRepository.UpdatePlayerIdentity(playerIdentity, currentMember!.Key, currentMember.UserName!).ConfigureAwait(false);

if (result.Status == PlayerIdentityUpdateResult.NotUnique)
{
Expand Down

0 comments on commit b20b7dc

Please sign in to comment.