Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions docs/guide/11-operations.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,19 +168,30 @@ protean:
- **worker jar**(`-worker` classifier, `protean-<ver>-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/<owner>/protean-worker:<ver>`, 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
Expand Down
15 changes: 13 additions & 2 deletions docs/guide/11-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,19 +168,30 @@ Three jars come out in `build/libs`, plus a worker container image built by Jib.
- **worker jar** (`-worker` classifier, `protean-<ver>-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/<owner>/protean-worker:<ver>`, 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
Expand Down
Loading