diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 83d6fdc..dff719c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -31,15 +31,22 @@ not break are ClassLoader/lifecycle hygiene and promotion-gate integrity. ## Verification -The operations guide (`docs/guide/11-operations.ko.md`) is the source of truth. -**Run `test` and `bootJar` as SEPARATE gradle invocations** — combining them can -OOM `LeakDiagnosisTest`. - -- [ ] `gradle clean test` passes (full regression) -- [ ] `gradle bootJar` assembles +The operations guide (`docs/guide/11-operations.md`, `.ko.md`) is the source of +truth. **Run `bootJar` and `test` as SEPARATE gradle invocations, bootJar first** — +combining them can OOM `LeakDiagnosisTest`, and running `clean test` first deletes +the `-boot.jar` that the container/isolation tests mount, so 16 of them self-skip +and the green covers less than it appears to. + +```bash +./gradlew clean bootJar +./gradlew test +``` + +- [ ] `gradle clean bootJar` assembles +- [ ] `gradle test` passes (full regression, run after the jar exists) - [ ] Docs updated if behaviour/config changed (README/README.ko, `docs/*`) -- [ ] Docker-dependent tests (Testcontainers/container isolation) run or - knowingly skipped (Docker absent) +- [ ] Container/isolation tests actually ran (not self-skipped). If they skipped, + say why — no `-boot.jar`, or no Docker. ## Related diff --git a/build.gradle b/build.gradle index 7839079..d4a61e9 100644 --- a/build.gradle +++ b/build.gradle @@ -424,6 +424,21 @@ tasks.named('test') { events 'passed', 'failed', 'skipped' showStandardStreams = true } + // The container/OS-isolation tests mount the fat -boot.jar and self-skip without it, so a run that never saw + // one is green over ~16 fewer tests than it appears to be. `clean test` is exactly that case: clean deletes the + // jar the tests are about to look for. CI does this deliberately (the isolation-test job builds the jar and runs + // them separately), but a developer running this locally has no second job to fall back on -- so say it out loud + // instead of leaving the gap to hide inside a skip count nobody reads. + def libsDir = layout.buildDirectory.dir('libs') + doFirst { + File dir = libsDir.get().asFile + boolean hasBootJar = dir.isDirectory() && dir.listFiles().any { it.name.endsWith('-boot.jar') } + if (!hasBootJar) { + logger.lifecycle("NOTE: no -boot.jar in build/libs, so the container/OS-isolation tests will self-skip." + + " Run './gradlew bootJar' first (as a separate invocation) to include them." + + " CI covers them in the isolation-test job.") + } + } } // The PoC application gate. Runs the criteria suite across isolation-mode x auto-provision combinations and writes diff --git a/docs/guide/11-operations.ko.md b/docs/guide/11-operations.ko.md index 74c8c97..59fbe73 100644 --- a/docs/guide/11-operations.ko.md +++ b/docs/guide/11-operations.ko.md @@ -168,19 +168,30 @@ protean: - **worker jar**(`-worker` classifier, `protean--worker.jar`): **sidecar** 워커 런타임의 process 트랙용 평평한 shaded uber-jar(Shadow). 이 트랙은 bare `java -cp` 로 띄우는데 `-boot.jar` 의 중첩 `BOOT-INF` 레이아웃으론 안 되므로 평평한 jar 가 필요하다. shade 시 Spring auto-config imports·JDBC 드라이버 service 파일을 concatenate 해 워커 컨텍스트가 정상 기동한다. - **worker image**(`ghcr.io//protean-worker:`, Jib 로 빌드 — `build/libs` 파일 아님): sidecar 워커 런타임의 container 트랙. worker jar 를 JDK 베이스 이미지의 `/app/` 에 담는다(워커가 런타임에 모듈 소스를 재컴파일하므로 `javac` 필요). [05. 격리 모드](05-isolation-modes.ko.md) 참고. -### bootJar 와 test 는 분리 실행(필수) +### bootJar 와 test 는 분리 실행 — 그리고 bootJar 가 먼저다(필수) `test` 태스크는 누수 카나리에서 soft reference 를 강제로 비우려 힙을 `maxHeapSize=512m` 로 제한한다. `bootJar`(fat jar 조립, 메모리 압박)와 `test` 를 한 gradle 호출에 묶으면 `LeakDiagnosisTest` 등이 collateral OOM 을 낼 수 있다. **반드시 분리 호출한다.** +호출은 둘, 그리고 **순서가 중요하다**: container·OS isolation 테스트는 fat `-boot.jar` 를 마운트하므로 그 jar 가 없으면 스스로 skip 한다(`OsIsolationTest` 의 `assumeTrue(bootJarExists())`). 그런데 `clean` 이 그 jar 를 지운다. `clean test` 를 먼저 돌리면 16건이 조용히 skip 된다 — 결과는 여전히 green 이지만 596건이 아니라 580건짜리 green 이고, 출력 어디에도 그 사실이 적히지 않는다. + ```bash # 나쁜 예 — 결합하면 OOM 위험 ./gradlew clean bootJar test -# 좋은 예 — 분리 +# 나쁜 예 — 분리는 했지만 clean 이 -boot.jar 를 지워서 +# container·isolation 16건이 skip 되고 green 의 범위가 좁아진다 ./gradlew clean test ./gradlew bootJar + +# 좋은 예 — 분리하고, 테스트가 필요로 할 때 jar 가 이미 있다 +./gradlew clean bootJar +./gradlew test ``` +로컬에서 하나의 호출에 전부 담는 데는 대가가 하나 있다: container 테스트가 Docker 를 함께 돌리므로 CI 의 분리 실행보다 부하가 크다. 그 부하에서 worker 기동 health check(`WorkerProcessIsolation.waitHealthy`)가 제한 시간을 넘겨, 단독으로는 통과하는 테스트가 실패할 수 있다 — 이 절을 쓰면서 전체 2회 중 1회 실제로 겪었다. 로컬 `maxRetries` 는 flake 를 감추지 않으려고 일부러 0 이므로, 그런 실패를 진짜로 취급하기 전에 해당 클래스만 다시 돌려 본다(`./gradlew test --tests 'org.htcom.protean.WorkerPoolTest'`). + +CI 는 일부러 다르게 하며 그 차이는 고칠 버그가 아니다: CI 의 `test` job 은 bootJar **없이** 돌려 container 테스트가 거기서 self-skip 되게 하고, 전용 `isolation-test` job 이 jar 를 만든 뒤 바로 그 테스트들만 돌린다(`.github/workflows/ci.yml`). 둘을 쪼개 두면 두 게이트를 따로 진단할 수 있고 병렬로도 돈다. 로컬에는 그런 분리가 없으므로, push 전 로컬 점검은 jar 를 먼저 만들지 않으면 그 16건을 아예 돌리지 않는 셈이 된다. + ### 발행 ```bash diff --git a/docs/guide/11-operations.md b/docs/guide/11-operations.md index b82f62e..4cdd26d 100644 --- a/docs/guide/11-operations.md +++ b/docs/guide/11-operations.md @@ -168,19 +168,30 @@ Three jars come out in `build/libs`, plus a worker container image built by Jib. - **worker jar** (`-worker` classifier, `protean--worker.jar`): a flat shaded uber-jar (Shadow) for the **sidecar** worker runtime's process track. That track launches with a bare `java -cp`, which a `-boot.jar`'s nested `BOOT-INF` layout cannot satisfy — hence a flat jar. Spring auto-configuration imports and JDBC driver service files are concatenated during shading so the worker context still comes up. - **worker image** (`ghcr.io//protean-worker:`, built by Jib — no `build/libs` file): the sidecar worker runtime's container track. Bundles the worker jar at `/app/` on a JDK base image (the worker recompiles module sources at runtime, so it needs `javac`). See [05. Isolation Modes](05-isolation-modes.md). -### bootJar and test must run separately (required) +### bootJar and test must run separately — and bootJar goes first (required) The `test` task limits the heap to `maxHeapSize=512m` to forcibly clear soft references in the leak canary. Bundling `bootJar` (fat-jar assembly, memory pressure) and `test` in one gradle invocation can make `LeakDiagnosisTest` and others throw a collateral OOM. **Always invoke them separately.** +Two invocations, and **the order matters**: the container and OS-isolation tests mount the fat `-boot.jar`, so they self-skip when it is absent (`assumeTrue(bootJarExists())` in `OsIsolationTest`). `clean` deletes it. Run `clean test` first and 16 tests skip silently — the run is still green, but green over 580 tests rather than 596, and nothing in the output says so. + ```bash # Bad — combining risks OOM ./gradlew clean bootJar test -# Good — separated +# Bad — separated, but `clean` removed the -boot.jar, so 16 container/isolation +# tests self-skip and the green is narrower than it looks ./gradlew clean test ./gradlew bootJar + +# Good — separated, and the jar exists by the time the tests need it +./gradlew clean bootJar +./gradlew test ``` +One consequence of putting everything in one local invocation: it is heavier than CI's split, because the container tests run Docker alongside the rest. Worker-spawn health checks (`WorkerProcessIsolation.waitHealthy`) can time out under that load and fail a test that passes on its own — observed once in two full runs while writing this section. Local `maxRetries` is 0 on purpose so flakiness stays visible, so re-run the class by itself (`./gradlew test --tests 'org.htcom.protean.WorkerPoolTest'`) before treating such a failure as real. + +CI does this differently on purpose, and that difference is not a bug to fix: its `test` job runs *without* a bootJar so the container tests self-skip there, and a dedicated `isolation-test` job builds the jar and then runs exactly those tests (`.github/workflows/ci.yml`). Splitting them keeps the two gates independently diagnosable and lets them run in parallel. Locally there is no such split, so a local pre-push check must put the jar first or it does not exercise those 16 tests at all. + ### Publish ```bash