fix: preserve prepared workflow state
This commit is contained in:
@@ -74,10 +74,12 @@ public sealed class LegacyPlayoutWorkflow
|
||||
private readonly IPlayoutEngine _engine;
|
||||
private readonly ILegacySceneCueProvider _cueProvider;
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
private readonly object _engineStatusSync = new();
|
||||
private IReadOnlyList<LegacyPlaylistEntry> _playlist = [];
|
||||
private ActivePage? _prepared;
|
||||
private ActivePage? _onAir;
|
||||
private int _engineClearRequested;
|
||||
private long _lastObservedEngineStatusSequence = long.MinValue;
|
||||
|
||||
public LegacyPlayoutWorkflow(
|
||||
IPlayoutEngine engine,
|
||||
@@ -98,13 +100,27 @@ public sealed class LegacyPlayoutWorkflow
|
||||
public void ObserveEngineStatus(PlayoutStatus status)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(status);
|
||||
if (!string.IsNullOrWhiteSpace(status.PreparedSceneName) ||
|
||||
!string.IsNullOrWhiteSpace(status.OnAirSceneName))
|
||||
var hasActiveScene = !string.IsNullOrWhiteSpace(status.PreparedSceneName) ||
|
||||
!string.IsNullOrWhiteSpace(status.OnAirSceneName);
|
||||
lock (_engineStatusSync)
|
||||
{
|
||||
if (status.Sequence <= _lastObservedEngineStatusSequence)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastObservedEngineStatusSequence = status.Sequence;
|
||||
// Availability/OnHello updates can report no scene while PREPARE still owns
|
||||
// the workflow gate. A later authoritative PREPARED/PROGRAM status must
|
||||
// cancel that deferred clear before the command publishes its page metadata.
|
||||
Interlocked.Exchange(ref _engineClearRequested, hasActiveScene ? 0 : 1);
|
||||
}
|
||||
|
||||
if (hasActiveScene)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _engineClearRequested, 1);
|
||||
if (!_gate.Wait(0))
|
||||
{
|
||||
return;
|
||||
@@ -537,6 +553,8 @@ public sealed class LegacyPlayoutWorkflow
|
||||
new(operation, PlayoutResultCode.Rejected, false, message, DateTimeOffset.UtcNow);
|
||||
|
||||
private void ApplyRequestedEngineClear()
|
||||
{
|
||||
lock (_engineStatusSync)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _engineClearRequested, 0) == 0)
|
||||
{
|
||||
@@ -548,6 +566,7 @@ public sealed class LegacyPlayoutWorkflow
|
||||
_playlist = [];
|
||||
State = EmptyState;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly LegacyPlayoutWorkflowState EmptyState = new(
|
||||
-1,
|
||||
|
||||
@@ -352,6 +352,79 @@ public sealed class LegacyPlayoutWorkflowTests
|
||||
Assert.DoesNotContain(engine.Calls, call => call.StartsWith("UpdateOnAir", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Prepare_EmptyThenPreparedEngineStatusWhileGateIsHeld_PreservesPublishedPageState()
|
||||
{
|
||||
var engine = new RecordingEngine();
|
||||
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
|
||||
{
|
||||
["5074"] = (6, ScenePageSize.Five)
|
||||
})
|
||||
{
|
||||
BuilderKey = "kospi-five-row",
|
||||
PreviewFields = [new("text", "title", "KOSPI")]
|
||||
};
|
||||
var workflow = new LegacyPlayoutWorkflow(engine, provider);
|
||||
engine.PrepareCallback = () =>
|
||||
{
|
||||
workflow.ObserveEngineStatus(engine.Status with
|
||||
{
|
||||
PreparedSceneName = null,
|
||||
OnAirSceneName = null,
|
||||
Sequence = 10
|
||||
});
|
||||
workflow.ObserveEngineStatus(engine.Status with
|
||||
{
|
||||
PreparedSceneName = "5074",
|
||||
OnAirSceneName = null,
|
||||
Sequence = 11
|
||||
});
|
||||
};
|
||||
|
||||
var result = await workflow.PrepareAsync([new("playlist-entry", "5074")], 0);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal("5074", workflow.State.PreparedCutCode);
|
||||
Assert.Null(workflow.State.OnAirCutCode);
|
||||
Assert.Equal("kospi-five-row", workflow.State.BuilderKey);
|
||||
Assert.Equal(0, workflow.State.PageIndexZeroBased);
|
||||
Assert.Equal(2, workflow.State.PageCount);
|
||||
Assert.Equal(5, workflow.State.PageSize);
|
||||
Assert.Equal(6, workflow.State.ItemCount);
|
||||
Assert.Equal(5, workflow.State.CurrentPageItemCount);
|
||||
Assert.Equal("playlist-entry", workflow.State.CurrentEntryId);
|
||||
Assert.Equal(LegacyWorkflowNextKind.PageNext, workflow.State.NextKind);
|
||||
Assert.Equal(provider.PreviewFields, workflow.State.PreviewFields);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ObserveEngineStatus_OlderEmptyStatusCannotOverrideNewerPreparedStatus()
|
||||
{
|
||||
var engine = new RecordingEngine();
|
||||
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
|
||||
{
|
||||
["5001"] = (1, null)
|
||||
});
|
||||
var workflow = new LegacyPlayoutWorkflow(engine, provider);
|
||||
Assert.True((await workflow.PrepareAsync([new("one", "5001")], 0)).IsSuccess);
|
||||
|
||||
workflow.ObserveEngineStatus(engine.Status with
|
||||
{
|
||||
PreparedSceneName = "5001",
|
||||
OnAirSceneName = null,
|
||||
Sequence = 20
|
||||
});
|
||||
workflow.ObserveEngineStatus(engine.Status with
|
||||
{
|
||||
PreparedSceneName = null,
|
||||
OnAirSceneName = null,
|
||||
Sequence = 19
|
||||
});
|
||||
|
||||
Assert.Equal("5001", workflow.State.PreparedCutCode);
|
||||
Assert.Equal("one", workflow.State.CurrentEntryId);
|
||||
}
|
||||
|
||||
private sealed class RecordingProvider(
|
||||
IReadOnlyDictionary<string, (int Count, ScenePageSize? Size)> definitions)
|
||||
: ILegacySceneCueProvider
|
||||
@@ -360,6 +433,10 @@ public sealed class LegacyPlayoutWorkflowTests
|
||||
|
||||
public Func<string, int, string>? SceneNameOverride { get; init; }
|
||||
|
||||
public string? BuilderKey { get; init; }
|
||||
|
||||
public IReadOnlyList<LegacyScenePreviewField>? PreviewFields { get; init; }
|
||||
|
||||
public Task<LegacySceneCuePage> CreatePageAsync(
|
||||
LegacyPlaylistEntry entry,
|
||||
int pageIndexZeroBased,
|
||||
@@ -375,7 +452,9 @@ public sealed class LegacyPlayoutWorkflowTests
|
||||
name,
|
||||
Mutations: [new PlayoutSetValue("page", $"p{pageIndexZeroBased}")]),
|
||||
definition.Count,
|
||||
definition.Size));
|
||||
definition.Size,
|
||||
BuilderKey,
|
||||
PreviewFields));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +466,8 @@ public sealed class LegacyPlayoutWorkflowTests
|
||||
|
||||
public Queue<PlayoutResultCode> UpdateOnAirResults { get; } = [];
|
||||
|
||||
public Action? PrepareCallback { get; set; }
|
||||
|
||||
public PlayoutStatus Status { get; } = new(
|
||||
PlayoutMode.DryRun,
|
||||
PlayoutConnectionState.DryRunReady,
|
||||
@@ -417,6 +498,7 @@ public sealed class LegacyPlayoutWorkflowTests
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Calls.Add("Prepare:" + Describe(cue));
|
||||
PrepareCallback?.Invoke();
|
||||
return Complete(PlayoutOperation.Prepare, PrepareResults);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user