/// <summary> /// Update the break's optimiser availability if the value has changed. /// </summary> /// <returns>Returns true if the break was updated. Also returns an information /// message.</returns> private bool UpdateOnlyBreakOptimiserAvailIfChanged(TBreak theBreak, Duration breakOptimiserAvailability) { var result = false; var infoMessage = new StringBuilder(128); infoMessage .Append(LogPrologue(theBreak.ExternalBreakRef)) .Append("Avail: ").Append(LogAsString.Log(theBreak.Avail)).Append("s; ") .Append("OptimiserAvail: ").Append(LogAsString.Log(theBreak.OptimizerAvail)).Append('s'); if (theBreak.HasAvailabilityChanged(breakOptimiserAvailability)) { theBreak.OptimizerAvail = breakOptimiserAvailability; infoMessage .Append("; OptimiserAvail now ") .Append(LogAsString.Log(breakOptimiserAvailability)) .Append('s'); result = true; } _logger.LogInformation(infoMessage.ToString()); return(result); }
private Dictionary <Guid, Duration> CalculateProgrammeBreakAvailabilities( IReadOnlyCollection <TBreak> programmeBreaks, IReadOnlyCollection <ISpotForBreakAvailCalculation> spots) { var programmeBreakAvailabilities = new Dictionary <Guid, Duration>(); foreach (var theBreak in programmeBreaks) { var bookedSpotDuration = spots .Where(spot => spot.ExternalBreakNo == theBreak.ExternalBreakRef) .Select(spot => spot.SpotLength) .Aggregate(Duration.Zero, (current, next) => current.Plus(next)); var breakAvailability = theBreak.Duration.Minus(bookedSpotDuration); _logger.LogInformation( $"[Ext. Break Ref {theBreak.ExternalBreakRef}] " + $"Duration {LogAsString.Log(theBreak.Duration)}s; " + $"calculated availability {LogAsString.Log(breakAvailability)}s" ); programmeBreakAvailabilities.Add(theBreak.Id, breakAvailability); } return(programmeBreakAvailabilities); }
/// <summary> /// Get the schedule for the given date or returns null. /// </summary> /// <param name="salesAreaName"></param> /// <param name="date"></param> /// <param name="scheduleRepository"></param> /// <returns></returns> private Schedule GetScheduleForUtcDate( string salesAreaName, DateTime date, IScheduleRepository scheduleRepository) { var result = scheduleRepository.GetSchedule(salesAreaName, date.Date); if (result is null) { _logger.LogWarning($"No schedule found for sales area {salesAreaName} on {LogAsString.Log(date.Date)}"); } else { _logger.LogInformation( $"Found schedule for sales area {salesAreaName} on " + $"{LogAsString.Log(date.Date)} (Schedule Id: {LogAsString.Log(result.Id)})" ); } return(result); }
public void Execute(DateTimeRange period, IEnumerable <SalesArea> salesAreas, CancellationToken cancellationToken = default) { if (!salesAreas.Any()) { _logger.LogWarning("No sales areas were passed to the calculator."); return; } var salesAreaNames = salesAreas .Select(sa => sa.Name) .ToList(); _logger.LogInformation($"Recalculating break availability for {LogAsString.Log(period.Start)} to {LogAsString.Log(period.End)}"); var allSpotsSubsetForAllSalesAreasForRunPeriodCollection = GetAllSpotSubsetsForAllSalesAreasForPeriod(period, salesAreaNames); if (allSpotsSubsetForAllSalesAreasForRunPeriodCollection.Count == 0) { _logger.LogWarning( $"Did not find any spots for the sales areas {string.Join("; ", salesAreaNames)} between {LogAsString.Log(period.Start)} to {LogAsString.Log(period.End)}" ); return; } var datesToProcess = GetDatesToProcess(period); var salesAreasToProcessInParallel = new ParallelOptions { MaxDegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1) }; var daysToProcessInParallel = new ParallelOptions { MaxDegreeOfParallelism = Math.Max(Environment.ProcessorCount / 2, 1) }; _ = Parallel.ForEach(salesAreaNames, salesAreasToProcessInParallel, salesAreaName => { _logger.LogInformation($"Calculating break availability and optimiser availability for sales area {salesAreaName}"); var spotSubsetForSalesAreaCollection = ImmutableList.CreateRange( allSpotsSubsetForAllSalesAreasForRunPeriodCollection .Where(s => s.SalesArea == salesAreaName)); var programmeSubsetForSalesAreaForRunPeriodCollection = GetProgrammesSubsetForPeriodForSalesArea(period, salesAreaName); _ = Parallel.ForEach(datesToProcess, daysToProcessInParallel, date => { var programmesForUtcDate = programmeSubsetForSalesAreaForRunPeriodCollection .Where(prog => prog.StartDateTime.Date == date.Date) .ToImmutableList(); if (programmesForUtcDate.Count == 0) { _logger.LogWarning($"No programmes found for sales area {salesAreaName} on {LogAsString.Log(date.Date)}"); return; } _logger.LogInformation( $"Found {LogAsString.Log(programmesForUtcDate.Count)} programmes " + $"for sales area {salesAreaName} on {LogAsString.Log(date.Date)}. " + $"(Programme Ids: {String.Join(",", programmesForUtcDate.Select(p => p.ProgrammeId))})" ); var anyProgrammesSpanningMidnight = programmesForUtcDate .Any(p => p.StartDateTime.Add(p.Duration.ToTimeSpan()) >= date.Date.AddDays(1)); Func <ISpotForBreakAvailCalculation, bool> condition; if (anyProgrammesSpanningMidnight) { condition = spot => spot.StartDateTime.Date == date.Date || spot.StartDateTime.Date == date.Date.AddDays(1); } else { condition = spot => spot.StartDateTime.Date == date.Date; } var spotsForUtcDate = spotSubsetForSalesAreaCollection .Where(condition) .ToList(); if (spotsForUtcDate.Count > 0) { _logger.LogInformation( $"Found {LogAsString.Log(spotsForUtcDate.Count)} spots " + $"for sales area {salesAreaName} on {LogAsString.Log(date.Date)}" ); } else { _logger.LogWarning($"No spots found for sales area {salesAreaName} on {LogAsString.Log(date.Date)}"); } using (var scope = _repositoryFactory.BeginRepositoryScope()) { var breakRepository = scope.CreateRepository <IBreakRepository>() ?? throw new NullReferenceException($"An instance of {nameof(IBreakRepository)} was not found."); var dateTo = anyProgrammesSpanningMidnight ? date.Date.AddDays(2).AddSeconds(-1) : date.Date.AddDays(1).AddSeconds(-1); var breaksForUtcDate = breakRepository.Search( date.Date, dateTo, salesAreaName ).ToList(); if (breaksForUtcDate.Count == 0) { _logger.LogWarning($"No breaks found for sales area {salesAreaName} on {LogAsString.Log(date.Date)}"); return; } _logger.LogInformation( $"Found {LogAsString.Log(breaksForUtcDate.Count)} break(s) for sales area {salesAreaName} " + $"on {LogAsString.Log(date.Date)}. " + $"[Break Ext. Refs: {breaksForUtcDate.ReducePropertyToCsv(x => x.ExternalBreakRef)}]" ); var scheduleRepository = scope.CreateRepository <IScheduleRepository>() ?? throw new NullReferenceException($"An instance of {nameof(IScheduleRepository)} was not found."); Schedule scheduleForUtcDate = GetScheduleForUtcDate( salesAreaName, date, scheduleRepository); CalculateBreaksAvailsForUtcDate( salesAreaName, programmesForUtcDate, spotsForUtcDate, breaksForUtcDate, scheduleForUtcDate, breakRepository, scheduleRepository); if (anyProgrammesSpanningMidnight) { var programmesSpanningMidnight = programmesForUtcDate .Where(p => p.StartDateTime.Add(p.Duration.ToTimeSpan()) >= date.Date) .ToList(); CopyUpdatedPostMidnightBreakAvails( salesAreaName, date.AddDays(1), programmesSpanningMidnight, breaksForUtcDate, breakRepository, scheduleRepository); } breakRepository.SaveChanges(); scheduleRepository.SaveChanges(); } }); }); }
/// <summary> /// Calculates the availability and optimiser availability for a programme's breaks, taking /// into account unplaced spots. /// </summary> /// <returns>Returns the number of breaks with reduced optimiser availability.</returns> public void Calculate( string salesAreaName, IReadOnlyCollection <IProgrammeForBreakAvailCalculation> programmesForUtcDate, IReadOnlyCollection <TBreak> breaksForProgrammes, IReadOnlyCollection <ISpotForBreakAvailCalculation> spotsForBreaks) { foreach (var programme in programmesForUtcDate) { var programmeBreaks = breaksForProgrammes .Where(theBreak => programme.DateTimeIsInProgramme(theBreak.ScheduledDate)) .ToList(); if (programmeBreaks.Count == 0) { _logger.LogInformation($"No breaks found for programme {LogAsString.Log(programme.ProgrammeId)}"); continue; } var breakExternalRefs = programmeBreaks.ReducePropertyToCsv(x => x.ExternalBreakRef); _logger.LogInformation( $"Found {LogAsString.Log(programmeBreaks.Count)} breaks for " + $"programme {LogAsString.Log(programme.ProgrammeId)} " + $"[Ext. Break Refs: {breakExternalRefs}]"); IReadOnlyDictionary <Guid, Duration> programmeBreakAvailabilities = CalculateProgrammeBreakAvailabilities( programmeBreaks, spotsForBreaks ); (int unplacedSpotsCount, Duration unplacedSpotsTotalDuration) = GatherProgrammeUnplacedSpotsDuration(programme, spotsForBreaks); _logger.LogInformation( $"Found {LogAsString.Log(unplacedSpotsCount)} unplaced spot(s) " + $"with total duration of {LogAsString.Log(unplacedSpotsTotalDuration)}s " + $"for programme {LogAsString.Log(programme.ProgrammeId)}" ); UpdateBreakAvailability(programmeBreaks, programmeBreakAvailabilities); if (unplacedSpotsTotalDuration == Duration.Zero) { continue; } var availByBreak = CalculateOptimiserAvailabilityForUnplacedSpots(programmeBreaks, unplacedSpotsTotalDuration); var countOfBreaksWithOptimizerAvailReducedForUnplacedSpots = UpdateBreakOptimiserAvailability(programmeBreaks, availByBreak); if (countOfBreaksWithOptimizerAvailReducedForUnplacedSpots == 0) { Duration breaksAvail = SumOfBreakAvailability(programmeBreaks); int programmeBreaksCount = programmeBreaks.Count; var warningMessage = new StringBuilder(256); warningMessage.Append($"There were {LogAsString.Log(unplacedSpotsCount)} unplaced spots "); warningMessage.Append($"for the programme {programme.ExternalReference} "); warningMessage.Append($"on {LogAsString.Log(programme.StartDateTime)} for sales area {salesAreaName}. "); warningMessage.Append("Unable to reduce the break optimizer availability "); warningMessage.Append($"for any of the {LogAsString.Log(programmeBreaksCount)} breaks. "); warningMessage.Append($"Ext. Break Refs: {breakExternalRefs}; "); warningMessage.Append($"Total break availability: {LogAsString.Log(breaksAvail)}s; "); warningMessage.Append($"Unplaced spots duration: {LogAsString.Log(unplacedSpotsTotalDuration)}s)"); _logger.LogWarning(warningMessage.ToString()); } } }
/// <summary> /// Calculate the optimiser availability for unplaced spots. /// </summary> /// <param name="programmeBreaks">A list of programme breaks.</param> /// <param name="unplacedSpotsDuration">Total duration of unplaced spots. The amount that we /// need to adjust the breaks by.</param> /// <returns></returns> private IReadOnlyList <Duration> CalculateOptimiserAvailabilityForUnplacedSpots( IEnumerable <TBreak> programmeBreaks, Duration unplacedSpotsDuration ) { var availByBreak = CurrentBreakAvailabilities(programmeBreaks); Duration unplacedSpotsLengthRemaining = unplacedSpotsDuration; // Check breaks and see if we can reduce availability bool done = false; while (!done) { if (AllUnplacedSpotDurationReallocated(unplacedSpotsLengthRemaining)) { _logger.LogInformation("All unplaced spot duration reallocated"); done = true; continue; } Duration breaksAvail = CurrentTotalBreakAvailability(availByBreak); if (NoBreakAvailability(breaksAvail)) { _logger.LogInformation( "No more break availability. " + $"Remaining unplaced spot duration: {LogAsString.Log(unplacedSpotsLengthRemaining)}s" ); done = true; continue; } int numberOfBreaksModified = 0; for (int breakIndex = 0; breakIndex < availByBreak.Count; breakIndex++) { if (availByBreak[breakIndex] <= Duration.Zero) { continue; } var availToReduceBreakBy = Duration.FromSeconds(15); if (unplacedSpotsLengthRemaining < availToReduceBreakBy) { availToReduceBreakBy = unplacedSpotsLengthRemaining; } if (availByBreak[breakIndex] >= availToReduceBreakBy) { availByBreak[breakIndex] = availByBreak[breakIndex].Minus(availToReduceBreakBy); unplacedSpotsLengthRemaining = unplacedSpotsLengthRemaining.Minus(availToReduceBreakBy); numberOfBreaksModified++; } if (unplacedSpotsLengthRemaining <= Duration.Zero) { done = true; break; } } if (numberOfBreaksModified == 0) { _logger.LogInformation("No breaks modified"); done = true; } } return(availByBreak); }
public void Execute( DateTimeRange period, IEnumerable <SalesArea> salesAreas, CancellationToken cancellationToken = default) { if (!salesAreas.Any()) { _logger.LogWarning("No sales areas found"); return; } var salesAreaNames = salesAreas.Select(x => x.Name).ToArray(); _logger.LogInformation($"Recalculating of break availability for {LogAsString.Log(period.Start)} to {LogAsString.Log(period.End)} started."); var hasErrors = false; try { Task.Run(async() => { var exceptionList = new List <Exception>(); var propagatorBlock = new BatchBlock <IBreakAvailability>( _recalculateOptions.UpdateBreakBatchSize, new GroupingDataflowBlockOptions { CancellationToken = cancellationToken }); using var calculator = new BreakAvailabilityCalculator( _logger, _tenantDbContextFactory, _recalculateOptions, propagatorBlock, cancellationToken); var updater = new BreakAvailabilityUpdater( _logger, _tenantDbContextFactory, _recalculateOptions, cancellationToken); var calculationBlock = new ActionBlock <(DateTimeRange Period, string SalesAreaName)>(tuple => calculator.CalculateAsync(tuple.Period, tuple.SalesAreaName), new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken, MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded, BoundedCapacity = _recalculateOptions.BoundedCalculateTaskCapacity, }); _ = updater.Start(propagatorBlock, () => calculationBlock.Complete()); try { var days = (period.End - period.Start).Days; if (period.End.TimeOfDay <= _defaultBroadcastDayEndTime) { days++; } try { foreach (var tuple in Enumerable.Range(0, days + 1) .Select(d => new DateTimeRange(period.Start.Date.AddDays(d), period.Start.Date.AddDays(d + 1))) .SelectMany(p => salesAreaNames.Select(salesAreaName => (Period: p, SalesAreaName: salesAreaName)))) { if (calculationBlock.Completion.IsCompleted) { break; } _ = await calculationBlock .SendAsync(tuple, cancellationToken) .ConfigureAwait(false); } } finally { calculationBlock.Complete(); exceptionList.AddRange( await calculationBlock.Completion .WaitWithTaskExceptionGatheringAsync() .ConfigureAwait(false) ); } } finally { calculator.Complete(); exceptionList.AddRange(await updater.WaitAsync().WaitWithTaskExceptionGatheringAsync() .ConfigureAwait(false)); } if (exceptionList.Count > 0) { await Task.FromException(new AggregateException(exceptionList)) .ConfigureAwait(false); } }, cancellationToken).GetAwaiter().GetResult(); } catch (OperationCanceledException) { _logger.LogInformation( $"Recalculating of break availability for {LogAsString.Log(period.Start)} to {LogAsString.Log(period.End)} cancelled."); hasErrors = true; } catch (Exception ex) { var message = $"Recalculating of break availability for {LogAsString.Log(period.Start)} to {LogAsString.Log(period.End)} finished with errors."; _logger.LogError(message, ex); hasErrors = true; throw new RecalculateBreakAvailabilityServiceException(message, ex); } finally { if (!hasErrors) { _logger.LogInformation( $"Recalculating of break availability for {LogAsString.Log(period.Start)} to {LogAsString.Log(period.End)} finished successfully."); } } }
public async Task CalculateAsync(DateTimeRange period, string salesAreaName) { var hasErrors = false; _logger.LogInformation( $"Break availability calculation for '{salesAreaName}' sales area " + $"on {LogAsString.Log(period.Start.Date)} has been started."); try { List <IProgrammeForBreakAvailCalculation> programmes = await GetProgrammesAsync(period, salesAreaName) .ConfigureAwait(false); if (programmes.Count == 0) { _logger.LogWarning( $"No programmes found for sales area {salesAreaName} " + $"on {LogAsString.Log(period.Start.Date)}"); return; } DateTimeRange periodCoveringWholeProgrammes = PeriodIncludingAnyProgrammeSpanningMidnight(period, programmes); var spotsQueryTask = GetSpots(periodCoveringWholeProgrammes, salesAreaName); var breaksQueryTask = GetBreaks(periodCoveringWholeProgrammes, salesAreaName); await Task .WhenAll(spotsQueryTask, breaksQueryTask) .ConfigureAwait(false); List <ISpotForBreakAvailCalculation> spots = spotsQueryTask.Result; if (spots.Count > 0) { _logger.LogInformation( $"Found {LogAsString.Log(spots.Count)} spots " + $"for sales area {salesAreaName} " + $"on {LogAsString.Log(period.Start.Date)}" ); } else { _logger.LogWarning( $"No spots found for sales area {salesAreaName} " + $"on {LogAsString.Log(period.Start.Date)}"); } _logger.LogInformation( $"Found {LogAsString.Log(programmes.Count)} programmes " + $"for sales area {salesAreaName} on {LogAsString.Log(period.Start.Date)}. " + $"(Programme Ids: {programmes.ReducePropertyToCsv(p => p.ProgrammeId)})" ); List <BreakAvailability> breaks = breaksQueryTask.Result; if (breaks.Count > 0) { string breakExternalRefs = breaks.ReducePropertyToCsv(x => x.ExternalBreakRef); _logger.LogInformation( $"Found {LogAsString.Log(breaks.Count)} break(s) for sales area {salesAreaName} " + $"on {LogAsString.Log(period.Start.Date)}. " + $"[Break Ext. Refs: {breakExternalRefs}]" ); } else { _logger.LogWarning( $"No breaks found for sales area {salesAreaName} " + $"on {LogAsString.Log(period.Start.Date)}"); } var updateBreakHandler = new BreakAvailabilityUpdateHandler(); var calculator = new BreakAndOptimiserAvailabilityCalculator <BreakAvailability>(_logger, updateBreakHandler); calculator.Calculate(salesAreaName, programmes, breaks, spots); if (updateBreakHandler.UpdatedBreaks.Count > 0) { _ = await _internalBlock .SendAsync(updateBreakHandler.UpdatedBreaks.Values.ToArray(), _cancellationToken) .ConfigureAwait(false); } } catch (OperationCanceledException) { _logger.LogInformation( $"Break availability calculation for '{salesAreaName}' sales area " + $"on {LogAsString.Log(period.Start.Date)} has been cancelled."); hasErrors = true; } catch (Exception ex) { _logger.LogError($"Break availability calculation for '{salesAreaName}' sales area " + $"on {LogAsString.Log(period.Start.Date)} has been finished with errors."); hasErrors = true; throw new BreakAvailabilityCalculatorException(period, salesAreaName, ex); } finally { if (!hasErrors) { _logger.LogInformation( $"Break availability calculation for '{salesAreaName}' sales area " + $"on {LogAsString.Log(period.Start.Date)} has been finished successfully."); } }
public BreakAvailabilityCalculatorException(DateTimeRange period, string salesAreaName, Exception innerException) : base( $"Break availability calculation for '{salesAreaName}' sales area on {LogAsString.Log(period.Start.Date)} has thrown an exception.", innerException) { }