fix: cap automatic playout refreshes

This commit is contained in:
2026-07-12 19:28:15 +09:00
parent 6e00955f1b
commit df60e09884
16 changed files with 603 additions and 37 deletions

View File

@@ -106,6 +106,7 @@ SDK가 기본 위치에 없다면 x64 SDK의 `TlbImp.exe` 절대 경로를 `-Tlb
| `*TimeoutMilliseconds` | 연결, 작업 및 해제 제한 시간 |
| `processPollIntervalMilliseconds` | `Tornado2` 접두사 프로세스 감시 주기 |
| `reconnect*` | 재연결 지연, 최대 횟수 및 활성화 여부 |
| `maximumAutomaticRefreshesPerTakeIn` | trusted local 자동 갱신 상한. `null`은 원본의 연속 갱신, `0`은 자동 갱신 비활성화, 양수는 TAKE IN/playlist NEXT로 시작한 epoch당 성공 dispatch 수 상한. 마지막 refresh의 `OnScenePlayed`까지 drain된 뒤 `CAPPED`로 보고 |
JSON보다 다음 환경 변수가 우선합니다.
@@ -134,7 +135,7 @@ MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS
MBN_STOCK_PLAYOUT_RECONNECT_ENABLED
```
`testSceneAllowlist` `trustedLiveOutputEnabled` 환경 변수로 변경할 수 없으며 로컬 설정 파일에서만 관리합니다. 이름은 기존 설정 호환을 위해 유지하지만 allowlist는 Test뿐 아니라 Live의 PREPARE, TAKE IN 재검사, NEXT와 timer refresh에도 적용되고 비어 있으면 실제 모드를 시작하지 않습니다. 공통 background/fade도 Web payload가 아니라 이 trusted 프로세스 설정 경계에서만 결정합니다. `PlayoutSceneCompositionFactory`는 첫 DryRun 또는 실제 PREPARE 전에 background asset의 상대 경로, 허용 확장자, 존재 여부와 reparse ancestry를 검사하며 실제 경로는 Web status/preview에 노출하지 않습니다. `SceneDirectory`는 Test/Live에서 존재하는 비-reparse 외부 디렉터리여야 하며, 엔진은 상대 `.t2s` 파일을 정규화해 이 루트 밖으로 나가는 경로를 거부합니다. scene file의 basename과 scene name 및 allowlist 항목도 서로 일치해야 합니다. 설정 파일은 실행 계정만 읽을 수 있도록 ACL을 제한합니다. 라이선스 키나 인증정보를 이 파일에 기록하지 않습니다.
`testSceneAllowlist`, `trustedLiveOutputEnabled`, `maximumAutomaticRefreshesPerTakeIn` 환경 변수로 변경할 수 없으며 로컬 설정 파일에서만 관리합니다. 이름은 기존 설정 호환을 위해 유지하지만 allowlist는 Test뿐 아니라 Live의 PREPARE, TAKE IN 재검사, NEXT와 timer refresh에도 적용되고 비어 있으면 실제 모드를 시작하지 않습니다. 자동 갱신 상한은 Web payload나 환경 변수로 늘릴 수 없고 COM dispatch 경계에서 다시 검사합니다. 공통 background/fade도 Web payload가 아니라 이 trusted 프로세스 설정 경계에서만 결정합니다. `PlayoutSceneCompositionFactory`는 첫 DryRun 또는 실제 PREPARE 전에 background asset의 상대 경로, 허용 확장자, 존재 여부와 reparse ancestry를 검사하며 실제 경로는 Web status/preview에 노출하지 않습니다. `SceneDirectory`는 Test/Live에서 존재하는 비-reparse 외부 디렉터리여야 하며, 엔진은 상대 `.t2s` 파일을 정규화해 이 루트 밖으로 나가는 경로를 거부합니다. scene file의 basename과 scene name 및 allowlist 항목도 서로 일치해야 합니다. 설정 파일은 실행 계정만 읽을 수 있도록 ACL을 제한합니다. 라이선스 키나 인증정보를 이 파일에 기록하지 않습니다.
### KTAP 포트와 Network Monitoring 판정

View File

@@ -69,6 +69,12 @@ BYE success marker가 0인 것은 계획의 허용 범위 0~1 안이며, 앱의
Round H 전에 Core 790, Infrastructure 65, Playout 374개 테스트를 Debug/Release x64에서 각각 모두 통과했고, 실제 Oracle/MariaDB read-only query 55건, Visual Studio 2026 Debug/Release x64와 package build, 서명·설치·package context 검사를 통과했다. 같은 signed MSIX의 package-context DryRun도 `5001 PREPARE → TAKE IN → 자동 refresh 정확히 1회 → 5074 page 1 → page 2 → TAKE OUT`과 최종 IDLE, `OutcomeUnknown=false`, KTAP 미호출을 확인했다.
### Round I 회귀 실패와 native 자동 갱신 상한
Round I `MBNWEB-20260712-I`는 계획 SHA-256 `D221841A15593A987FB944648F0ACE0AC5940936AEE16C74DC9B901A2F45B332`로 CONNECT 1회와 PREPARE 5001/page 1, TAKE IN 1회를 실행했다. 5001 삼성전자 PGM 표시는 확인했지만, 실시간 관찰 자동화가 TAKE OUT까지 하나의 중단 없는 작업으로 묶이지 않아 승인된 자동 refresh 최대 1회를 넘어 9회가 실행됐다. 최종 누적 PLAY/`OnScenePlayed`는 각각 10회(초기 TAKE IN 1 + refresh 9)였다. NEXT와 다른 scene LOAD는 없었고 FAILURE/ERROR는 0이었다. 승인된 TAKE OUT 1회로 STOPALL/UNLOAD 5001 각 1회 성공, PGM black, BYE request 1, 앱 정상 종료를 확인했다. 회차 결과는 `FAIL`, `OutcomeUnknown=false`, vendor command retry 0이며 결과 SHA-256은 `E5FD13E7FCDF87B6ADCFB696EFF2E957B70ECA821A424FC15EE41043CFA0E37E`이다.
이 실패 뒤 `maximumAutomaticRefreshesPerTakeIn` trusted JSON-only 상한을 추가했다. 기본 `null`은 원본 연속 갱신을 유지하고, 실제 검증 설정의 `1`은 scheduler와 실제 refresh dispatch 경계에서 두 번째 실행을 이중 차단한다. 상태는 `refreshCompletedCount`, `refreshMaximumCount`, `refreshLimitReached`를 제공하며, 마지막 refresh `OnScenePlayed`가 drain되기 전에는 count가 상한에 도달해도 `refreshLimitReached=false`를 유지한다. 검증은 `completed=1`, `maximum=1`, `limitReached=true`, `refreshActive=false`, `playCompletionPending=false`를 모두 확인해야 한다. 변경된 package는 Round I 승인/계획을 재사용할 수 없으며 새 package hash, 새 회차 계획과 새 CONNECT/PREPARE/TAKE IN 승인이 필요하다.
이 실제 Live PGM 증거는 회차에 허용된 5001과 5074에 한정된다. 35개 scene builder 전체 동등성은 자동 테스트, 55-query 실데이터 smoke와 [`SCENE_EQUIVALENCE.md`](SCENE_EQUIVALENCE.md) 매트릭스로 유지하며, 나머지 scene이 실제 PGM에서 각각 송출됐다는 의미로 기록하지 않는다.
## 화면/계약과 운영 검증의 분리
@@ -222,9 +228,10 @@ PREPARE는 다음 native 순서를 수행해야 한다.
### 5. Timer refresh
- 승인 범위에 포함된 경우에만 자동 refresh를 관찰한다. 현재 안전 스케줄러는 직전 `OnScenePlayed` callback이 lifecycle queue에서 drain된 뒤에만 one-shot cooldown을 시작한다. 5001의 첫 cooldown은 온전한 2초이고, 성공한 refresh 뒤에는 callback drain 후 온전한 3초 recurring cooldown이다. 따라서 실제 dispatch 간격은 callback/drain 시간과 cooldown의 합이며, 원본의 절대 3초 cadence보다 의도적으로 길다.
- 실제 검증 회차는 trusted JSON에 `maximumAutomaticRefreshesPerTakeIn`을 승인 횟수와 동일하게 고정한다. Web과 환경 변수는 이 값을 변경할 수 없으며, 상한 도달 후 마지막 callback까지 drain되면 loop가 fault 없이 `CAPPED`로 끝난다.
- 이 callback-aware cooldown은 recurring refresh를 제거한 것이 아니다. missed tick을 누적하거나 따라잡지 않고 합치며, refresh는 최대 하나만 in-flight다. 각 실행은 current entry/page의 fresh DB DTO를 사용해 retained on-air scene에 `BeginTransaction` → mutation → `QueryVariables` → `EndTransaction` → `Prepare(10)` → `Play(10)`을 한 번 수행한다.
- timer refresh는 scene-level background와 fade effect를 다시 적용하지 않는다.
- 앱의 `refreshActive`, `refreshNextAt`, `refreshLastSuccessAt`, fault code/message와 PGM 데이터를 함께 기록한다.
- 앱의 `refreshActive`, `refreshNextAt`, `refreshLastSuccessAt`, `refreshCompletedCount`, `refreshMaximumCount`, `refreshLimitReached`, fault code/message와 PGM 데이터를 함께 기록한다.
- refresh가 실패하거나 timeout/unknown이면 fault latch가 켜지고 반복이 즉시 끝나야 한다. TAKE OUT 이외의 명령을 시도하지 않는다.
- 명확히 성공한 TAKE OUT 뒤 native refresh state가 reset되면 전용 refresh fault marker만 사라져야 한다. `OutcomeUnknown`, `WEB_TIMEOUT` 또는 native quarantine 표시는 함께 reset되면 안 된다.
- operator NEXT는 먼저 현재 refresh epoch를 원자적으로 stop한다. 그 시점에 Play callback이 pending이면 DB activity 대기나 scene query 전에 `PLAY_CALLBACK_PENDING`으로 즉시 fail-fast하고 자동 재시도하지 않는다. 명령 gate가 바로 풀리므로 TAKE OUT은 계속 사용할 수 있다. callback이 완료된 cooldown 구간에서는 NEXT를 정상 실행할 수 있다.