/// <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);
        }
Ejemplo n.º 3
0
        /// <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);
        }
Ejemplo n.º 4
0
        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);
        }
Ejemplo n.º 7
0
        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.");
                }
            }
        }
Ejemplo n.º 8
0
        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.");
                }
            }
Ejemplo n.º 9
0
 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)
 {
 }