fix block playout history regression (#2343)
* minor tweaks * fix block change detection bug * history cleanup cleanup
This commit is contained in:
@@ -72,7 +72,9 @@ public static class BlockPlayoutChangeDetectionTests
|
|||||||
collectionEtags = collectionEtags.Add(collectionKey1, JsonConvert.SerializeObject(collectionKey1));
|
collectionEtags = collectionEtags.Add(collectionKey1, JsonConvert.SerializeObject(collectionKey1));
|
||||||
collectionEtags = collectionEtags.Add(collectionKey2, JsonConvert.SerializeObject(collectionKey2));
|
collectionEtags = collectionEtags.Add(collectionKey2, JsonConvert.SerializeObject(collectionKey2));
|
||||||
|
|
||||||
|
var buildStart = GetLocalDate(2024, 1, 17);
|
||||||
Tuple<List<EffectiveBlock>, List<PlayoutItem>> result = BlockPlayoutChangeDetection.FindUpdatedItems(
|
Tuple<List<EffectiveBlock>, List<PlayoutItem>> result = BlockPlayoutChangeDetection.FindUpdatedItems(
|
||||||
|
buildStart,
|
||||||
playoutItems,
|
playoutItems,
|
||||||
itemBlockKeys,
|
itemBlockKeys,
|
||||||
effectiveBlocks,
|
effectiveBlocks,
|
||||||
@@ -85,6 +87,65 @@ public static class BlockPlayoutChangeDetectionTests
|
|||||||
result.Item2.Count.ShouldBe(0);
|
result.Item2.Count.ShouldBe(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Should_Not_Remove_Items_From_Outside_The_Scheduling_Window()
|
||||||
|
{
|
||||||
|
// This test demonstrates a bug where playout items from the past are removed
|
||||||
|
// because their generating block is not in the current scheduling window.
|
||||||
|
|
||||||
|
DateTimeOffset dateUpdated = DateTimeOffset.Now;
|
||||||
|
List<Block> blocks = Blocks(dateUpdated);
|
||||||
|
var template1 = new Template { Id = 1, DateUpdated = dateUpdated.UtcDateTime };
|
||||||
|
var playoutTemplate1 = new PlayoutTemplate { Id = 10, DateUpdated = dateUpdated.UtcDateTime };
|
||||||
|
|
||||||
|
var template2 = new Template { Id = 2, DateUpdated = dateUpdated.UtcDateTime };
|
||||||
|
var playoutTemplate2 = new PlayoutTemplate { Id = 20, DateUpdated = dateUpdated.UtcDateTime };
|
||||||
|
|
||||||
|
Block block1 = blocks[0]; // Yesterday's block
|
||||||
|
Block block2 = blocks[1]; // Today's block
|
||||||
|
|
||||||
|
var blockKey1 = new BlockKey(block1, template1, playoutTemplate1);
|
||||||
|
var blockKey2 = new BlockKey(block2, template2, playoutTemplate2);
|
||||||
|
|
||||||
|
var collectionKey1 = CollectionKey.ForBlockItem(block1.Items.Head());
|
||||||
|
var collectionKey2 = CollectionKey.ForBlockItem(block2.Items.Head());
|
||||||
|
|
||||||
|
// Playout item from yesterday
|
||||||
|
PlayoutItem playoutItem1 = PlayoutItem(blockKey1, collectionKey1, GetLocalDate(2024, 1, 17).AddHours(9));
|
||||||
|
|
||||||
|
List<PlayoutItem> playoutItems = [playoutItem1];
|
||||||
|
Dictionary<PlayoutItem, BlockKey> itemBlockKeys =
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
{ playoutItem1, blockKey1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Effective blocks for today - does not include yesterday's block
|
||||||
|
List<EffectiveBlock> effectiveBlocks =
|
||||||
|
[
|
||||||
|
new(block2, blockKey2, GetLocalDate(2024, 1, 18).AddHours(9), 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
Map<CollectionKey, string> collectionEtags = LanguageExt.Map<CollectionKey, string>.Empty;
|
||||||
|
collectionEtags = collectionEtags.Add(collectionKey1, JsonConvert.SerializeObject(collectionKey1));
|
||||||
|
collectionEtags = collectionEtags.Add(collectionKey2, JsonConvert.SerializeObject(collectionKey2));
|
||||||
|
|
||||||
|
var buildStart = GetLocalDate(2024, 1, 18);
|
||||||
|
Tuple<List<EffectiveBlock>, List<PlayoutItem>> result = BlockPlayoutChangeDetection.FindUpdatedItems(
|
||||||
|
buildStart,
|
||||||
|
playoutItems,
|
||||||
|
itemBlockKeys,
|
||||||
|
effectiveBlocks,
|
||||||
|
collectionEtags);
|
||||||
|
|
||||||
|
// should schedule today's block
|
||||||
|
result.Item1.Count.ShouldBe(1);
|
||||||
|
result.Item1.Head().Block.ShouldBe(block2);
|
||||||
|
|
||||||
|
// should NOT remove yesterday's item
|
||||||
|
result.Item2.Count.ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Block> Blocks(DateTimeOffset dateUpdated)
|
private static List<Block> Blocks(DateTimeOffset dateUpdated)
|
||||||
{
|
{
|
||||||
List<Block> blocks =
|
List<Block> blocks =
|
||||||
|
|||||||
@@ -77,12 +77,13 @@ public class BlockPlayoutBuilder(
|
|||||||
result.ItemsToRemove.Add(item.Id);
|
result.ItemsToRemove.Add(item.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// // remove old items
|
// remove old items
|
||||||
// // importantly, this should not remove their history
|
// importantly, this should not remove their history
|
||||||
// result = result with { RemoveBefore = start };
|
result = result with { RemoveBefore = start };
|
||||||
|
|
||||||
(List<EffectiveBlock> updatedEffectiveBlocks, List<PlayoutItem> playoutItemsToRemove) =
|
(List<EffectiveBlock> updatedEffectiveBlocks, List<PlayoutItem> playoutItemsToRemove) =
|
||||||
BlockPlayoutChangeDetection.FindUpdatedItems(
|
BlockPlayoutChangeDetection.FindUpdatedItems(
|
||||||
|
start,
|
||||||
referenceData.ExistingItems,
|
referenceData.ExistingItems,
|
||||||
itemBlockKeys,
|
itemBlockKeys,
|
||||||
blocksToSchedule,
|
blocksToSchedule,
|
||||||
@@ -231,6 +232,7 @@ public class BlockPlayoutBuilder(
|
|||||||
PlaybackOrder = blockItem.PlaybackOrder,
|
PlaybackOrder = blockItem.PlaybackOrder,
|
||||||
Index = enumerator.State.Index,
|
Index = enumerator.State.Index,
|
||||||
When = currentTime.UtcDateTime,
|
When = currentTime.UtcDateTime,
|
||||||
|
Finish = playoutItem.FinishOffset.UtcDateTime,
|
||||||
Key = historyKey,
|
Key = historyKey,
|
||||||
Details = HistoryDetails.ForMediaItem(mediaItem)
|
Details = HistoryDetails.ForMediaItem(mediaItem)
|
||||||
};
|
};
|
||||||
@@ -333,30 +335,15 @@ public class BlockPlayoutBuilder(
|
|||||||
PlayoutBuildResult result)
|
PlayoutBuildResult result)
|
||||||
{
|
{
|
||||||
IEnumerable<PlayoutHistory> allItemsToDelete = referenceData.PlayoutHistory
|
IEnumerable<PlayoutHistory> allItemsToDelete = referenceData.PlayoutHistory
|
||||||
.Append(result.AddedHistory)
|
.GroupBy(h => h.Key)
|
||||||
.GroupBy(h => (h.BlockId, h.Key))
|
|
||||||
.SelectMany(group => group
|
.SelectMany(group => group
|
||||||
.Filter(h => h.When < start.UtcDateTime)
|
.Filter(h => h.Finish < start.UtcDateTime)
|
||||||
.OrderByDescending(h => h.When)
|
.OrderByDescending(h => h.Finish)
|
||||||
.Tail());
|
.Tail());
|
||||||
|
|
||||||
var addedToRemove = new System.Collections.Generic.HashSet<PlayoutHistory>();
|
|
||||||
|
|
||||||
foreach (PlayoutHistory delete in allItemsToDelete)
|
foreach (PlayoutHistory delete in allItemsToDelete)
|
||||||
{
|
{
|
||||||
if (delete.Id > 0)
|
result.HistoryToRemove.Add(delete.Id);
|
||||||
{
|
|
||||||
result.HistoryToRemove.Add(delete.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
addedToRemove.Add(delete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addedToRemove.Count > 0)
|
|
||||||
{
|
|
||||||
result.AddedHistory.RemoveAll(addedToRemove.Contains);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ internal static class BlockPlayoutChangeDetection
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Tuple<List<EffectiveBlock>, List<PlayoutItem>> FindUpdatedItems(
|
public static Tuple<List<EffectiveBlock>, List<PlayoutItem>> FindUpdatedItems(
|
||||||
|
DateTimeOffset buildStart,
|
||||||
List<PlayoutItem> playoutItems,
|
List<PlayoutItem> playoutItems,
|
||||||
Dictionary<PlayoutItem, BlockKey> itemBlockKeys,
|
Dictionary<PlayoutItem, BlockKey> itemBlockKeys,
|
||||||
List<EffectiveBlock> blocksToSchedule,
|
List<EffectiveBlock> blocksToSchedule,
|
||||||
@@ -124,8 +125,8 @@ internal static class BlockPlayoutChangeDetection
|
|||||||
Log.Logger.Debug("Earliest block id: {Id} => {Value}", blockId, value);
|
Log.Logger.Debug("Earliest block id: {Id} => {Value}", blockId, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// find affected playout items
|
// find affected playout items (importantly, from within the effective window)
|
||||||
foreach (PlayoutItem item in playoutItems)
|
foreach (PlayoutItem item in playoutItems.Where(pi => pi.StartOffset >= buildStart))
|
||||||
{
|
{
|
||||||
if (!itemBlockKeys.TryGetValue(item, out BlockKey blockKey))
|
if (!itemBlockKeys.TryGetValue(item, out BlockKey blockKey))
|
||||||
{
|
{
|
||||||
@@ -144,7 +145,8 @@ internal static class BlockPlayoutChangeDetection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Tuple(updatedBlocks.ToList(), playoutItems.Filter(i => updatedItemIds.Contains(i.Id)).ToList());
|
var itemsToRemove = playoutItems.Filter(i => updatedItemIds.Contains(i.Id)).ToList();
|
||||||
|
return Tuple(updatedBlocks.ToList(), itemsToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RemoveItemAndHistory(
|
public static void RemoveItemAndHistory(
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ public class BlockPlayoutFillerBuilder(
|
|||||||
PlaybackOrder = PlaybackOrder.Shuffle,
|
PlaybackOrder = PlaybackOrder.Shuffle,
|
||||||
Index = enumerator.State.Index,
|
Index = enumerator.State.Index,
|
||||||
When = current.UtcDateTime,
|
When = current.UtcDateTime,
|
||||||
|
Finish = filler.FinishOffset.UtcDateTime,
|
||||||
Key = historyKey,
|
Key = historyKey,
|
||||||
Details = HistoryDetails.ForMediaItem(mediaItem)
|
Details = HistoryDetails.ForMediaItem(mediaItem)
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+6328
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Populate_PlayoutHistory_Finish : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
@"UPDATE PlayoutHistory
|
||||||
|
SET Finish =
|
||||||
|
(SELECT PlayoutItem.Finish
|
||||||
|
FROM PlayoutItem
|
||||||
|
WHERE PlayoutItem.PlayoutId = PlayoutHistory.PlayoutId
|
||||||
|
AND PlayoutItem.Start = PlayoutHistory.`When`)
|
||||||
|
WHERE EXISTS (SELECT 1
|
||||||
|
FROM PlayoutItem
|
||||||
|
WHERE PlayoutItem.PlayoutId = PlayoutHistory.PlayoutId
|
||||||
|
AND PlayoutItem.Start = PlayoutHistory.`When`);");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6163
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Populate_PlayoutHistory_Finish : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
@"UPDATE PlayoutHistory
|
||||||
|
SET Finish =
|
||||||
|
(SELECT PlayoutItem.Finish
|
||||||
|
FROM PlayoutItem
|
||||||
|
WHERE PlayoutItem.PlayoutId = PlayoutHistory.PlayoutId
|
||||||
|
AND PlayoutItem.Start = PlayoutHistory.`When`)
|
||||||
|
WHERE EXISTS (SELECT 1
|
||||||
|
FROM PlayoutItem
|
||||||
|
WHERE PlayoutItem.PlayoutId = PlayoutHistory.PlayoutId
|
||||||
|
AND PlayoutItem.Start = PlayoutHistory.`When`);");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,13 +32,13 @@ public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerCh
|
|||||||
|
|
||||||
// for debugging by fast-forwarding a playout
|
// for debugging by fast-forwarding a playout
|
||||||
// [HttpPost("/api/channels/{channelNumber}/playout/continue")]
|
// [HttpPost("/api/channels/{channelNumber}/playout/continue")]
|
||||||
// public async Task<IActionResult> ContinuePlayout(string channelNumber)
|
// public async Task<IActionResult> ContinuePlayout(string channelNumber, [FromQuery] int days = 1)
|
||||||
// {
|
// {
|
||||||
// Option<int> maybePlayoutId = await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber));
|
// Option<int> maybePlayoutId = await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber));
|
||||||
// foreach (int playoutId in maybePlayoutId)
|
// foreach (int playoutId in maybePlayoutId)
|
||||||
// {
|
// {
|
||||||
// DateTimeOffset start = DateTimeOffset.Now;
|
// DateTimeOffset start = DateTimeOffset.Now;
|
||||||
// for (int i = 0; i < 24; i++)
|
// for (int i = 0; i < 24 * days; i++)
|
||||||
// {
|
// {
|
||||||
// await workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue, start));
|
// await workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue, start));
|
||||||
// start += TimeSpan.FromHours(1);
|
// start += TimeSpan.FromHours(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user