Files
Gnuboard7/tests/scenarios/core-update-spawn-failure-mode.yaml
T
HeuJung ffab451b4a fix(core,installer): dev vendor 설치본의 코어 업데이트 중단 수정 — stale 패키지 매니페스트 3계층
Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지
않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를
--no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는
`composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를
찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상
종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다.
(sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10)

3계층으로 막는다.

 1. 부모 — spawn 직전 PackageManifestCacheHelper::clear
 2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다.
 이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전
 자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다.
 3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로
 축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을
 incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI
 자체에 도달할 수 없다).

업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고
CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의
복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다.

실측 중 드러난 결함 2건을 함께 고쳤다.

 - ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아
 Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다.
 - updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다
 (Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다).

인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인
카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를
정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다.
코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3).

코어 7.0.10 → 7.0.11.
2026-09-08 09:46:57 +09:00

173 lines
15 KiB
YAML

# audit:allow test-scenario-coverage reason: 본 매니페스트는 코어 업데이트 spawn 실패 / silent skip / stale 메모리 가드 인프라의 시나리오 매트릭스 SSoT. 핵심 회귀 가드는 test_files 의 통과 테스트로 커버.
feature: 코어 업데이트 spawn 자식 실패 fail-fast + [STEPS_EXECUTED] silent skip 가드 + stale 메모리 가드
description: |
공개 이슈 gnuboard/g7#28 (beta.3 → beta.4 업그레이드 도중 `Call to undefined method
ensureWritableDirectories()` fatal) 의 발현 메커니즘은 spawn 자식 프로세스 실패 →
부모 in-process fallback 진입 → 부모 메모리의 stale 클래스가 신규 메서드 호출 시
fatal.
본 매니페스트는 spawn 자식 실패의 4가지 분기 (proc_open 비활성 / 자원 생성 실패 /
자식 비정상 종료 / 자식 silent skip) 각각에 대해 spawn_failure_mode (abort/fallback)
가 정상 동작하는지 + stale 메모리 가드가 in-process fallback 진입 시점에 abort/fallback
분기를 일관 적용하는지의 cross product 회귀 가드.
구성 (정책 결정 2026-05-11):
1. `spawn_failure_mode` 기본값 `abort` — 4분기 모두 UpgradeHandoffException throw.
2. `[STEPS_EXECUTED]` stdout 라인 프로토콜 — 자식이 실행한 step 수(count) + 범위 내
발견된 스텝 파일 수(discovered) 명시. 부모는 신호 미수신 또는 executed=0 &&
discovered>0 (스텝 파일이 있는데 실행 못함) 을 silent skip 으로 판정. executed=0 &&
discovered=0 (스텝 파일 부재) 은 정상 통과 — from<to 여도 실패 아님 (스텝 불필요 릴리즈).
3. `runUpgradeSteps` 진입 직후 stale 메모리 가드 — config('app.version') < toVersion
이면 mode=abort 시 throw, mode=fallback 시 warning log.
4. V-1 안전 작성 패턴 docs + audit rule `upgrade-step-vone-safety` (warning) —
upgrade step 안의 `app(*Service|*Manager|*Repository::class)` 호출을 reviewer 에게 surface.
분기 개정 (discovered 축 세분화): `child_silent_skip_zero_steps` 를 discovered 축으로
세분화. 범위 내 스텝 파일이 애초에 없어 0건인 경우(discovered=0)를 정상 통과로 재분류하여,
스텝이 필요 없는 패치 릴리즈(예: 7.0.0→7.0.1) 를 실패로 오판하던 결함을 차단.
재실행 권한 안내 (§8, resume_exec_context 축): spawn 자식 실패로 스텝이 미실행 상태로
남으면 운영자에게 `core:execute-upgrade-steps` 재실행을 안내한다. sudo(root) 로 실행 중이면
안내받은 명령을 root 로 그대로 재실행할 경우 스텝 생성 파일이 root 소유가 되어 이후 웹서버
요청이 쓰기 실패한다. 실행 사용자(root 여부) × 웹서버 계정 식별성으로 4분기 안내:
non_root(명령만) / root_web_known(sudo -u {계정} + 경고) / root_web_symmetric(root 서비스
구성, 명령만) / root_web_unknown(placeholder + 계정명 미상 경고). 공유 호스팅에서 웹서버·
PHP·실행 유저가 동일하면 non_root 로 처리된다.
axes:
spawn_branch: [proc_open_disabled, proc_open_resource_fail, child_abnormal_exit, child_silent_skip_no_signal, child_zero_steps_with_discovered, child_zero_steps_no_step_files]
spawn_failure_mode: [abort, fallback]
from_to_relation: [from_lt_to, from_eq_to_forced] # version_compare 분기
parent_memory_version: [equal_to_to, less_than_to, greater_than_to] # stale 가드 트리거 조건
memory_version_source: [env_app_version, config_only_env_absent] # 가드 판독 출처 — env 우선(spawn 자식 계약), env 부재 시 config 폴백
child_config_cache_state: [cached_from_version, no_cache] # 자식이 이전 버전 config 캐시로 부팅하는가 (7.0.9→7.0.10 실사례)
steps_executed_signal: [present_positive, present_zero, absent] # 자식의 [STEPS_EXECUTED] count 발행 상태
steps_discovered_signal: [discovered_positive, discovered_zero, absent] # 범위 내 발견된 스텝 파일 수 (신버전 자식만 발행, 구버전=absent)
child_process_origin: [beta_5_plus, beta_4_or_earlier] # 이전 버전 자식 (silent skip 시뮬레이션)
upgrade_step_v_one_pattern: [app_service_call, app_manager_call, app_repository_call, local_only]
audit_allow_inline: [present, absent] # 면제 주석 적용 여부
symlink_target_state: [valid, broken, windows_no_privilege] # symlink 보존 분기 (§1 연동)
staging_source_shape: [inner_extracted_root, staging_root_itself, external_source_dir] # cleanupPending 이 받는 경로 형태 — ZIP·GitHub / --local·--source 복제 / --source 외부
staging_leftover_content: [empty_skeleton, has_files, non_core_dir] # 자식 청소 술어 — 빈 껍데기만 대상
resume_exec_context: [non_root, root_web_known, root_web_symmetric, root_web_unknown] # 핸드오프 재실행 권한 안내 분기 (실행 사용자 × 웹서버 계정 식별성)
package_manifest_state: [fresh, stale_dev_provider] # bootstrap/cache/packages.php 가 이전 설치본(dev composer)의 provider 를 담고 있는가
parent_generation: [pre_7_0_11, 7_0_11_plus] # 부모가 spawn 직전 매니페스트를 비우는 세대인가 (이미 배포된 7.0.9·7.0.10 은 비우지 않는다)
child_self_heal_flag: [present, absent] # 자식이 G7_UPDATE_IN_PROGRESS 를 물려받았는가 (자가 치유 게이트)
process_context: [update_tree, long_lived_outside_update] # 버전 판독 범위 — 상주 프로세스(artisan serve·큐 워커)는 트리 밖이다
vendor_dev_packages: [present, absent, unknown] # 운영 vendor 에 require-dev 가 섞여 있는가 (installed.json 판정)
composer_step_branch: [reinstalled, skipped_unchanged] # Step 6 이 vendor 를 교체했는가 스킵했는가
exclusions:
- { spawn_branch: proc_open_disabled, parent_memory_version: equal_to_to, reason: "proc_open 비활성 시점에 부모는 항상 fromVersion 메모리 — equal 케이스 시뮬 불가" }
- { from_to_relation: from_eq_to_forced, spawn_branch: child_zero_steps_with_discovered, reason: "from==to + force 케이스는 step 0건이 정상 동작이므로 silent skip 가드 미트리거" }
- { child_process_origin: beta_4_or_earlier, steps_executed_signal: present_positive, reason: "beta.5 이전 자식은 [STEPS_EXECUTED] 신호 자체를 모름 — present_positive 시뮬 불가" }
- { child_process_origin: beta_4_or_earlier, steps_discovered_signal: discovered_positive, reason: "discovered 필드는 신버전 자식만 발행 — 구버전 자식은 항상 absent" }
- { child_process_origin: beta_4_or_earlier, steps_discovered_signal: discovered_zero, reason: "동일 — 구버전 자식은 discovered 필드 미발행(absent)" }
- { spawn_branch: child_zero_steps_no_step_files, steps_discovered_signal: discovered_positive, reason: "스텝 파일 부재 브랜치는 정의상 discovered=0 — positive 모순" }
- { spawn_branch: child_zero_steps_with_discovered, steps_discovered_signal: discovered_zero, reason: "스텝 파일 존재 브랜치는 정의상 discovered>0 — zero 모순" }
- { parent_generation: 7_0_11_plus, package_manifest_state: stale_dev_provider, reason: "7.0.11+ 부모는 spawn 직전에 매니페스트를 비우므로 자식이 stale 을 보는 상태 자체가 성립하지 않는다 — 계층 ② 검증은 pre_7_0_11 부모에서만 의미가 있다" }
- { process_context: long_lived_outside_update, child_self_heal_flag: present, reason: "플래그를 물고 있으면 정의상 업데이트 트리 안이다 — 모순" }
- { vendor_dev_packages: unknown, composer_step_branch: skipped_unchanged, reason: "판정 불가(installed.json 부재)에서는 감지 로그 자체가 출력되지 않아 분기가 구분되지 않는다" }
effects:
# §2 spawn_failure_mode + failSpawnWithMode
- failSpawnWithMode_abort_throws_UpgradeHandoffException_with_resume_command
- failSpawnWithMode_fallback_returns_false_with_warning_log
- spawnUpgradeStepsProcess_proc_open_disabled_dispatches_to_failSpawnWithMode
- spawnUpgradeStepsProcess_proc_open_resource_fail_dispatches_to_failSpawnWithMode
- spawnUpgradeStepsProcess_child_abnormal_exit_dispatches_to_failSpawnWithMode
# §2.1 [STEPS_EXECUTED] silent skip 가드 (count + discovered)
- ExecuteUpgradeStepsCommand_emits_STEPS_EXECUTED_with_count_and_discovered_on_normal_completion
- ExecuteUpgradeStepsCommand_does_not_emit_STEPS_EXECUTED_on_handoff_exit
- spawnUpgradeStepsProcess_parses_STEPS_EXECUTED_with_positive_count_returns_true
- spawnUpgradeStepsProcess_missing_STEPS_EXECUTED_dispatches_to_failSpawnWithMode
# discovered 기반 분기 — executed=0 을 discovered 로 구분
- handleSpawnExit_zero_executed_zero_discovered_returns_true # 케이스 B: 스텝 파일 부재 정상 통과
- handleSpawnExit_zero_executed_positive_discovered_dispatches_to_failSpawnWithMode # 케이스 A: gnuboard/g7#28 silent skip
- handleSpawnExit_zero_executed_null_discovered_from_lt_to_dispatches_to_failSpawnWithMode # 구버전 자식 레거시 판정
- spawnUpgradeStepsProcess_no_step_files_returns_true_even_when_from_lt_to # 7.0.0→7.0.1 실증
- runUpgradeSteps_notifies_discovered_zero_when_no_step_in_range
- spawnUpgradeStepsProcess_zero_steps_with_from_eq_to_forced_returns_true
# §6 stale 메모리 가드
- runUpgradeSteps_throws_UpgradeHandoffException_when_memory_lt_to_with_abort_mode
- runUpgradeSteps_logs_warning_when_memory_lt_to_with_fallback_mode
- runUpgradeSteps_proceeds_silently_when_memory_eq_or_gt_to
- runUpgradeSteps_reads_env_APP_VERSION_before_config_so_spawn_child_with_stale_config_cache_passes_guard
- runUpgradeSteps_falls_back_to_config_version_when_env_APP_VERSION_absent
# config 캐시 부팅 자식 (2026-09-06 전수조사) — 부모는 spawn 전에 캐시를 비우고, 자식은 캐시 부팅이면 디스크 config 를 읽는다
- spawnUpgradeStepsProcess_clears_config_cache_before_proc_open
- execute_upgrade_steps_child_reads_update_config_from_disk_when_config_is_cached
- route_cache_rebuild_preserves_container_instance
- runUpgradeSteps_resume_command_format_matches_execute_upgrade_steps_signature
# §1 symlink 보존 (CoreBackupHelper 위임 경로 포함)
- copyDirectory_preserves_symlink_target_pointer_on_linux
- copyDirectory_falls_back_to_directory_copy_on_symlink_failure
- copyDirectory_replaces_existing_directory_with_symlink_when_source_is_symlink
- removeOrphanItems_unlinks_orphan_symlinks_without_recursive_delete
# §7 V-1 audit rule
- upgrade_step_vone_safety_warns_on_app_service_call_in_upgrade_step
- upgrade_step_vone_safety_warns_on_app_manager_call_in_upgrade_step
- upgrade_step_vone_safety_warns_on_app_repository_call_in_upgrade_step
- upgrade_step_vone_safety_skips_when_audit_allow_inline_present
# §8 핸드오프 재실행 권한 안내 (sudo/root × 웹서버 계정 식별성 4분기)
- renderResumeGuidance_non_root_prints_command_verbatim_without_permission_warning
- renderResumeGuidance_root_web_known_prefixes_sudo_u_and_warns_with_account_name
- renderResumeGuidance_root_web_symmetric_prints_command_verbatim
- renderResumeGuidance_root_web_unknown_uses_placeholder_and_generic_warning
# §9 격리 디렉토리 정리 3층 (부모 루트째 삭제 / 자식 빈 껍데기 청소 / 스냅샷 제외) + 완료 안내문
- cleanupPending_removes_whole_staging_root_when_given_inner_source_path
- sweepEmptyStagingDirectories_removes_empty_core_dirs_and_keeps_dirs_with_files
- snapshotOwnershipDetailed_excludes_current_run_staging_root
- execute_bundled_updates_child_sweeps_empty_staging_directories_left_by_parent
- apply_mode_incremental_prune_hint_does_not_reference_rollback_command
# #658 stale 패키지 매니페스트 3계층 (부모 선정리 / 자식 자가 치유 / 버전 판독 범위)
- spawnUpgradeStepsProcess_clears_package_manifests_before_proc_open
- spawn_child_boots_with_regenerated_package_manifest_when_parent_left_stale_dev_manifest
- bootstrap_app_unlinks_stale_package_manifest_when_update_flag_present
- bootstrap_app_leaves_package_manifest_untouched_without_update_flag
- PackageManifestCacheHelper_clear_unlinks_packages_and_services_at_configured_paths
- clearAllCaches_rebuilds_package_manifest_via_helper
- CoreUpdateContext_isInProgress_detects_env_flag_or_update_argv
- getCoreVersion_prefers_env_APP_VERSION_only_inside_update_tree
- getCoreVersion_ignores_env_APP_VERSION_outside_update_tree
- getCoreVersion_falls_back_to_config_when_env_absent_inside_update_tree
# #658 상주 큐 워커 재시작 신호
- core_update_step11_signals_queue_restart
test_files:
- tests/Feature/Console/CoreUpdateCommandSpawnFailureTest.php
- tests/Feature/Console/Commands/ExecuteUpgradeStepsStandaloneTest.php
- tests/Unit/Services/CoreUpdateServiceFreshDiskConfigTest.php
- tests/Unit/Support/RouteCacheHelperContainerTest.php
- tests/Feature/Console/CoreUpdateCommandHandoffTest.php
- tests/Feature/Console/CoreUpdateResumeGuidanceTest.php
- tests/Feature/Upgrades/MultiVersionUpgradePathTest.php
- tests/Unit/Extension/Helpers/FilePermissionHelperSymlinkTest.php
- tests/Unit/Services/CoreUpdateServiceStagingCleanupTest.php
- tests/Feature/Console/ExecuteBundledUpdatesCommandTest.php
- tests/Feature/Console/CoreUpdateCommandStalePackageManifestTest.php
- tests/Unit/Support/PackageManifestCacheHelperTest.php
- tests/Unit/Support/CoreUpdateContextTest.php
- tests/Unit/Extension/CoreVersionCheckerEnvPriorityTest.php
- tests/Unit/Services/CoreUpdateServiceQueueRestartTest.php
# 본 매니페스트의 axes cross product 는 800+ 케이스이나, 실제 회귀 가드는 test_files 의
# 통과 테스트들이 SSoT — 매니페스트는 매트릭스 SSoT 역할.
#
# 잔존 결함 (계획서 §9 명시, 본 매트릭스에서 제외):
# 9.1: proc_open 비활성 + spawn_failure_mode=fallback 사용자의 V-1 fatal
# → mode=fallback 자체가 호환 옵션이며, V-1 위험 잔존은 fallback 의 본질.
# 매트릭스 검증 대상 아님.
# 9.2: Windows 환경의 public/storage symlink 보존
# → axes.symlink_target_state=windows_no_privilege 케이스로 표현되며,
# symlink 생성 자체가 PHP 권한 부족으로 실패 → 일반 디렉토리 폴백.
# 회귀 테스트가 markTestSkipped 로 표시.
# 9.3: OPCache file_cache 활성 환경의 stale 자식 디스크 캐시
# → 본 매니페스트 범위 밖 (인프라 설정).
# 9.6: beta.1/2 사용자의 beta.5 직접 점프 fatal
# → axes.parent_memory_version=less_than_to 케이스 일부 시뮬레이션 가능하나,
# 실 fatal 은 이전 버전 디스크의 CoreUpdateCommand 가 호출되는 시점이라
# 본 브랜치 코드의 영향력 범위 밖. 단계적 업그레이드 가이드 (CHANGELOG Upgrade Notice).