Skip to content

Commit

Permalink
Improve performance for creating fixtures with MatchScheduler
Browse files Browse the repository at this point in the history
* Planned matches of a tournament are cached for faster checks of available dates
* Extend debug logging
* Update tenant settings for development to include RefereeRuleSet

Update MatchScheduler
  • Loading branch information
axunonb committed Jan 20, 2024
1 parent c396adf commit d1eb1f7
Show file tree
Hide file tree
Showing 15 changed files with 202 additions and 129 deletions.
5 changes: 5 additions & 0 deletions League.Demo/Configuration/Tenant.Default.Development.config
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,10 @@
<MustBeSet>True</MustBeSet>
</HomeVenue>
</TeamRuleSet>
<!-- Rules for referee master data -->
<RefereeRuleSet>
<!-- Rules for organizing referees -->
<RefereeType>None</RefereeType>
</RefereeRuleSet>
</TournamentContext>
</TenantContext>
5 changes: 5 additions & 0 deletions League.Demo/Configuration/Tenant.Default.Production.config
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,10 @@
<MustBeSet>True</MustBeSet>
</HomeVenue>
</TeamRuleSet>
<!-- Rules for referee master data -->
<RefereeRuleSet>
<!-- Rules for organizing referees -->
<RefereeType>None</RefereeType>
</RefereeRuleSet>
</TournamentContext>
</TenantContext>
5 changes: 5 additions & 0 deletions League.Demo/Configuration/Tenant.OtherOrg.Development.config
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,10 @@
<MustBeSet>True</MustBeSet>
</HomeVenue>
</TeamRuleSet>
<!-- Rules for referee master data -->
<RefereeRuleSet>
<!-- Rules for organizing referees -->
<RefereeType>Home</RefereeType>
</RefereeRuleSet>
</TournamentContext>
</TenantContext>
5 changes: 5 additions & 0 deletions League.Demo/Configuration/Tenant.OtherOrg.Production.config
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,10 @@
<MustBeSet>True</MustBeSet>
</HomeVenue>
</TeamRuleSet>
<!-- Rules for referee master data -->
<RefereeRuleSet>
<!-- Rules for organizing referees -->
<RefereeType>Home</RefereeType>
</RefereeRuleSet>
</TournamentContext>
</TenantContext>
5 changes: 5 additions & 0 deletions League.Demo/Configuration/Tenant.TestOrg.Development.config
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,10 @@
<MustBeSet>True</MustBeSet>
</HomeVenue>
</TeamRuleSet>
<!-- Rules for referee master data -->
<RefereeRuleSet>
<!-- Rules for organizing referees -->
<RefereeType>Home</RefereeType>
</RefereeRuleSet>
</TournamentContext>
</TenantContext>
5 changes: 5 additions & 0 deletions League.Demo/Configuration/Tenant.TestOrg.Production.config
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,10 @@
<MustBeSet>True</MustBeSet>
</HomeVenue>
</TeamRuleSet>
<!-- Rules for referee master data -->
<RefereeRuleSet>
<!-- Rules for organizing referees -->
<RefereeType>Home</RefereeType>
</RefereeRuleSet>
</TournamentContext>
</TenantContext>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using SD.LLBLGen.Pro.ORMSupportClasses;
using TournamentManager.DAL.EntityClasses;
using TournamentManager.DAL.HelperClasses;
using TournamentManager.Plan;

namespace TournamentManager.Data;

Expand Down Expand Up @@ -35,6 +36,46 @@ public async Task<EntityCollection<AvailableMatchDateEntity>> GetAvailableMatchD
await da.FetchEntityCollectionAsync(qp, cancellationToken);
da.CloseConnection();

_logger.LogDebug("Fetched {count} available match dates for tournament {tournamentId}.", available.Count, tournamentId);

return available;
}

/// <summary>
/// Removes entries in AvailableMatchDates database table.
/// </summary>
/// <param name="tournamentId">The tournament ID.</param>
/// <param name="clear">Which entries to delete for the tournament.</param>
/// <param name="cancellationToken"></param>
/// <returns>Returns the number of deleted records.</returns>
public async Task<int> ClearAsync(long tournamentId, MatchDateClearOption clear, CancellationToken cancellationToken)
{
var deleted = 0;

// tournament is always in the filter
var filterAvailable = new RelationPredicateBucket();
filterAvailable.PredicateExpression.Add(AvailableMatchDateFields.TournamentId == tournamentId);

if ((clear & MatchDateClearOption.All) == MatchDateClearOption.All)
{
deleted = await _dbContext.AppDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
null, cancellationToken);
}
else if ((clear & MatchDateClearOption.OnlyAutoGenerated) == MatchDateClearOption.OnlyAutoGenerated)
{
filterAvailable.PredicateExpression.AddWithAnd(AvailableMatchDateFields.IsGenerated == true);
deleted = await _dbContext.AppDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
filterAvailable, cancellationToken);
}
else if ((clear & MatchDateClearOption.OnlyManual) == MatchDateClearOption.OnlyManual)
{
filterAvailable.PredicateExpression.AddWithAnd(AvailableMatchDateFields.IsGenerated == false);
deleted = await _dbContext.AppDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
filterAvailable, cancellationToken);
}

_logger.LogDebug("Deleted {deleted} available match dates for tournament {tournamentId}.", deleted, tournamentId);

return deleted;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public async Task<EntityCollection<ExcludeMatchDateEntity>> GetExcludedMatchDate
/// but only for the team or round.
/// </summary>
/// <remarks>
/// Same behavior as with <see cref="TournamentManager.Plan.AvailableMatchDates.IsExcludedDate"/>.
/// Same conditions as with <see cref="TournamentManager.Plan.AvailableMatchDates.IsExcludedDate"/>
/// which uses a cached list of <see cref="ExcludeMatchDateEntity"/>s.
/// </remarks>
/// <param name="match">The <see cref="MatchEntity"/> where RoundId and TeamId are taken.</param>
/// <param name="tournamentId">The TournamentId to filter the result.</param>
Expand All @@ -58,7 +59,7 @@ public async Task<EntityCollection<ExcludeMatchDateEntity>> GetExcludedMatchDate
public virtual async Task<ExcludeMatchDateEntity?> GetExcludedMatchDateAsync(MatchEntity match,
long tournamentId, CancellationToken cancellationToken)
{
if (!(match.PlannedStart.HasValue && match.PlannedEnd.HasValue)) return null;
if (match is not { PlannedStart: not null, PlannedEnd: not null }) return null;

using var da = _dbContext.GetNewAdapter();
var tournamentFilter = new PredicateExpression(
Expand Down
4 changes: 2 additions & 2 deletions TournamentManager/TournamentManager/Data/GenericRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public virtual async Task DeleteEntitiesAsync<T>(T entitiesToDelete, Cancellatio
var count = await da.DeleteEntityCollectionAsync(entitiesToDelete, cancellationToken);
}

public virtual async Task<int> DeleteEntitiesDirectlyAsync(Type entityType, IRelationPredicateBucket filterBucket, CancellationToken cancellationToken)
public virtual async Task<int> DeleteEntitiesDirectlyAsync(Type entityType, IRelationPredicateBucket? filterBucket, CancellationToken cancellationToken)
{
using var da = _dbContext.GetNewAdapter();
return await da.DeleteEntitiesDirectlyAsync(entityType, filterBucket, cancellationToken);
Expand All @@ -97,4 +97,4 @@ public virtual async Task<int> DeleteEntitiesUsingConstraintAsync<T>(IPredicateE
var bucket = new RelationPredicateBucket(uniqueConstraintFilter);
return await da.DeleteEntitiesDirectlyAsync(typeof(T), bucket, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ public interface ITournamentContext
/// Rules for team master data
/// </summary>
TeamRules TeamRuleSet { get; set; }
}

/// <summary>
/// Rules for referee master data
/// </summary>
RefereeRules RefereeRuleSet { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ public class TournamentContext : ITournamentContext
public TeamRules TeamRuleSet { get; set; } = new();

/// <summary>
/// Rules for organizing referees.
/// Rules for referee master data.
/// </summary>
[YAXLib.Attributes.YAXComment("Rules for organizing referees")]
[YAXLib.Attributes.YAXComment("Rules for referee master data")]
public RefereeRules RefereeRuleSet { get; set; } = new();
}

Expand Down Expand Up @@ -166,14 +166,14 @@ public class TeamRules
}

/// <summary>
/// Rules for organizing referees.
/// Rules for referee master data.
/// </summary>
public class RefereeRules
{
/// <summary>
/// Rules for teams' home match time
/// </summary>
[YAXLib.Attributes.YAXComment("Rules for organizing referees")]
[YAXLib.Attributes.YAXComment("Rule for organizing referees")]
public Plan.RefereeType RefereeType { get; set; } = Plan.RefereeType.None;
}

Expand Down
72 changes: 30 additions & 42 deletions TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using TournamentManager.DAL.EntityClasses;
using TournamentManager.DAL.HelperClasses;
using SD.LLBLGen.Pro.ORMSupportClasses;
using Microsoft.Extensions.Logging;
using TournamentManager.Data;
using TournamentManager.MultiTenancy;
Expand Down Expand Up @@ -62,39 +61,22 @@ await _appDb.AvailableMatchDateRepository.GetAvailableMatchDatesAsync(


/// <summary>
/// Removes entries in AvailableMatchDates database table.
/// Removes entries in <see cref="AvailableMatchDates"/> database table and internal cache.
/// </summary>
/// <param name="clear">Which entries to delete for the tournament.</param>
/// <param name="cancellationToken"></param>
/// <returns>Returns the number of deleted records.</returns>
public async Task<int> ClearAsync(MatchDateClearOption clear, CancellationToken cancellationToken)
{
var deleted = 0;
var deleted =
await _appDb.AvailableMatchDateRepository.ClearAsync(_tenantContext.TournamentContext.MatchPlanTournamentId,
clear, cancellationToken);

// tournament is always in the filter
var filterAvailable = new RelationPredicateBucket();
filterAvailable.PredicateExpression.Add(AvailableMatchDateFields.TournamentId ==
_tenantContext.TournamentContext.MatchPlanTournamentId);

if (clear == MatchDateClearOption.All)
{
deleted = await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
null!, cancellationToken);
_generatedAvailableMatchDateEntities.Clear();
}
else if (clear == MatchDateClearOption.OnlyAutoGenerated)
{
filterAvailable.PredicateExpression.AddWithAnd(AvailableMatchDateFields.IsGenerated == true);
deleted = await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
filterAvailable, cancellationToken);
if((clear & MatchDateClearOption.OnlyAutoGenerated) == MatchDateClearOption.OnlyAutoGenerated)
_generatedAvailableMatchDateEntities.Clear();
}
else if (clear == MatchDateClearOption.OnlyManual)
{
filterAvailable.PredicateExpression.AddWithAnd(AvailableMatchDateFields.IsGenerated == false);
deleted = await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
filterAvailable, cancellationToken);
}

if ((clear & MatchDateClearOption.OnlyManual) == MatchDateClearOption.OnlyManual)
_availableMatchDateEntities.Clear();

return deleted;
}
Expand All @@ -112,16 +94,19 @@ private static bool IsVenueAndDateDefined(TeamEntity team)
/// Verifies, that the given <paramref name="matchDateTimeUtc"></paramref> is within the <see cref="RoundLegEntity"/> date
/// boundaries, <b>and</b> it is not excluded, <b>and</b> the venue is not occupied by another match.
/// </summary>
private async Task<bool> IsDateUsable(DateTime matchDateTimeUtc, RoundLegEntity roundLeg, TeamEntity team, CancellationToken cancellationToken)
private bool IsDateUsable(DateTime matchDateTimeUtc, RoundLegEntity roundLeg, IEnumerable<MatchEntity> tournamentMatches, TeamEntity team)
{
var plannedDuration = _tenantContext.TournamentContext.FixtureRuleSet.PlannedDurationOfMatch;

// Todo: This code creates heavy load on the database
return IsDateWithinRoundLegDateTime(roundLeg, matchDateTimeUtc)
var isAvailable = IsDateWithinRoundLegDateTime(roundLeg, matchDateTimeUtc)
&& !IsExcludedDate(matchDateTimeUtc, roundLeg.RoundId, team.Id)
&& !await IsVenueOccupiedByMatchAsync(
&& !IsVenueOccupiedByMatch(
new DateTimePeriod(matchDateTimeUtc, matchDateTimeUtc.Add(plannedDuration)),
team.VenueId!.Value, cancellationToken);
team.VenueId!.Value, tournamentMatches);

_logger.LogDebug("Venue '{venueId}' is available for '{matchDateTimeUtc}': {isAvailable}", team.VenueId, matchDateTimeUtc, isAvailable);

return isAvailable;
}


Expand All @@ -131,9 +116,10 @@ private async Task<bool> IsDateUsable(DateTime matchDateTimeUtc, RoundLegEntity
/// are not <see langword="null"/>.
/// </summary>
/// <param name="round"></param>
/// <param name="tournamentMatches"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task GenerateNewAsync(RoundEntity round, CancellationToken cancellationToken)
public async Task GenerateNewAsync(RoundEntity round, EntityCollection<MatchEntity> tournamentMatches, CancellationToken cancellationToken)
{
await Initialize(cancellationToken);

Expand Down Expand Up @@ -165,7 +151,7 @@ public async Task GenerateNewAsync(RoundEntity round, CancellationToken cancella
// check whether the calculated date
// is within the borders of round legs (if any) and is not marked as excluded

if (await IsDateUsable(matchDateTimeUtc, roundLeg, team, cancellationToken))
if (IsDateUsable(matchDateTimeUtc, roundLeg, tournamentMatches, team))
{
var av = new AvailableMatchDateEntity
{
Expand Down Expand Up @@ -195,8 +181,7 @@ public async Task GenerateNewAsync(RoundEntity round, CancellationToken cancella
_logger.LogDebug("Generated {Count} UTC dates for HomeTeams:", _generatedAvailableMatchDateEntities.Count);
_logger.LogDebug("{Generated}\n", _generatedAvailableMatchDateEntities.Select(gen => (gen.HomeTeamId, gen.MatchStartTime)));

// save to the persistent storage
// await _appDb.GenericRepository.SaveEntitiesAsync(_generatedAvailableMatchDateEntities, true, false, cancellationToken);
// Note: Generated dates are not saved to the database, but only used for the current run.
}

/// <summary>
Expand All @@ -222,11 +207,13 @@ private List<EntityCollection<TeamEntity>> GetListOfTeamsWithSameVenue(RoundEnti
return listTeamsWithSameVenue;
}

private async Task<bool> IsVenueOccupiedByMatchAsync(DateTimePeriod matchTime, long venueId,
CancellationToken cancellationToken)
private static bool IsVenueOccupiedByMatch(DateTimePeriod matchTime, long venueId,
IEnumerable<MatchEntity> tournamentMatches)
{
return (await _appDb.VenueRepository.GetOccupyingMatchesAsync(venueId, matchTime,
_tenantContext.TournamentContext.MatchPlanTournamentId, cancellationToken)).Count == 0;
return tournamentMatches.Any(m => m.VenueId == venueId &&
m is { IsComplete: false, PlannedStart: not null, PlannedEnd: not null }
&& (m.PlannedStart <= matchTime.End &&
matchTime.Start <= m.PlannedEnd)); // overlapping periods
}

private static bool IsDateWithinRoundLegDateTime(RoundLegEntity leg, DateTime queryDate)
Expand All @@ -249,7 +236,8 @@ private static DateTime IncrementDateUntilDayOfWeek(DateTime date, DayOfWeek day
/// but only for the team or round.
/// </summary>
/// <remarks>
/// Same behavior as with <see cref="ExcludedMatchDateRepository.GetExcludedMatchDateAsync"/>.
/// Same conditions as with <see cref="ExcludedMatchDateRepository.GetExcludedMatchDateAsync"/>
/// which gets a <see cref="ExcludeMatchDateEntity"/>s from the database, if one exists, or <see langword="null"/>.
/// </remarks>
/// <param name="queryDate">Date to test, whether it is excluded.</param>
/// <param name="roundId">OR excluded on the round level. If <see langword="null" />, there is no round restriction.</param>
Expand Down Expand Up @@ -278,7 +266,7 @@ private bool IsExcludedDate(DateTime queryDate, long? roundId, long? teamId)
;
}

public List<DateTime> GetGeneratedAndManualAvailableMatchDateDays(RoundLegEntity leg)
internal List<DateTime> GetGeneratedAndManualAvailableMatchDateDays(RoundLegEntity leg)
{
var result = _generatedAvailableMatchDateEntities.Union(_availableMatchDateEntities)
.Where(gen => gen.TournamentId == _tenantContext.TournamentContext.MatchPlanTournamentId
Expand All @@ -291,7 +279,7 @@ public List<DateTime> GetGeneratedAndManualAvailableMatchDateDays(RoundLegEntity
return result;
}

public List<AvailableMatchDateEntity> GetGeneratedAndManualAvailableMatchDates(long homeTeamId,
internal List<AvailableMatchDateEntity> GetGeneratedAndManualAvailableMatchDates(long homeTeamId,
DateTimePeriod datePeriod, List<DateTime>? excludedDates)
{
if (datePeriod is not { Start: not null, End: not null }) throw new ArgumentNullException(nameof(datePeriod));
Expand Down
4 changes: 2 additions & 2 deletions TournamentManager/TournamentManager/Plan/MatchCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace TournamentManager.Plan;

/// <summary>
/// Class to create matches for a group of participants.
/// The round robin system is applied, i.e. all participants in the group play each other.
/// The round-robin system is applied, i.e. all participants in the group play each other.
/// </summary>
/// <typeparam name="TP">The <see langword="struct"/> participant type.</typeparam>
/// <typeparam name="TR">The <see langword="struct"/> referee type.</typeparam>
Expand Down Expand Up @@ -118,7 +118,7 @@ public ParticipantCombinations<TP, TR> GetCombinations(LegType legType)
{
CreateCombinations(_tenantContext.TournamentContext.RefereeRuleSet.RefereeType);

return ((legType == LegType.First) ? _participantCombinationsFirstLeg : _participantCombinationsReturnLeg);
return (legType == LegType.First) ? _participantCombinationsFirstLeg : _participantCombinationsReturnLeg;
}

/// <summary>
Expand Down
Loading

0 comments on commit d1eb1f7

Please sign in to comment.