@nestarc/inbox 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ {
2
+ "checkedAt": "2026-10-28T13:15:03.229Z",
3
+ "packageVersion": "0.2.0",
4
+ "brokerImage": "rabbitmq:4-alpine",
5
+ "brokerVersion": "4.4.0",
6
+ "postgresVersion": "16.15",
7
+ "nodeVersion": "v24.11.1",
8
+ "brokerSigkillRestarts": 3,
9
+ "elapsedMs": 21752,
10
+ "results": [
11
+ {
12
+ "scenario": "SIGKILL after receipt commit before ACK",
13
+ "redelivered": true,
14
+ "receipt": "duplicate",
15
+ "committedEffects": 1
16
+ },
17
+ {
18
+ "scenario": "admission failure, confirmed retry transfer, broker SIGKILL, TTL redelivery",
19
+ "transportRetries": 1,
20
+ "committedEffects": 1
21
+ },
22
+ {
23
+ "scenario": "persistent admission failure exhausts transport retry budget into DLQ",
24
+ "acceptanceFailures": 4,
25
+ "transportRetries": 3,
26
+ "committedReceipts": 0,
27
+ "committedEffects": 0
28
+ },
29
+ {
30
+ "scenario": "invalid JSON and identity conflict reach transport DLQ",
31
+ "deadLetters": 2,
32
+ "newCommittedEffects": 0
33
+ },
34
+ {
35
+ "scenario": "Inbox retry ownership survives broker and service restart",
36
+ "transientAttempts": 2,
37
+ "poisonAttempts": 3,
38
+ "committedEffects": {
39
+ "transient": 1,
40
+ "poison": 0
41
+ },
42
+ "poisonStatus": "DEAD"
43
+ }
44
+ ],
45
+ "finalCounts": {
46
+ "PENDING": 0,
47
+ "PROCESSING": 0,
48
+ "RETRYABLE": 0,
49
+ "COMPLETED": 5,
50
+ "DEAD": 1
51
+ }
52
+ }
@@ -0,0 +1,30 @@
1
+ # RabbitMQ restart and retry recovery
2
+
3
+ On 2026-10-28, the [operational harness](../scripts/test-broker-recovery.mjs) passed all five scenarios with RabbitMQ 4.4.0, PostgreSQL 16.15, and Node 24.11.1. It killed the actual RabbitMQ process with `SIGKILL` three times and restarted the same container with its persistent data volume. The successful run took 21.752 seconds, excluding the outer PostgreSQL setup. [Raw result](broker-recovery-results.json).
4
+
5
+ | Scenario | Verified result |
6
+ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | Receipt and business transaction commit, then broker dies before ACK | RabbitMQ marks the delivery as redelivered; `accept()` returns the same completed receipt as a duplicate; exactly one business effect remains |
8
+ | Admission backpressure, confirmed transfer to the retry queue, then broker dies | Persistent retry message survives; TTL dead-letter routing returns it to the main queue with retry and `x-death` evidence; processing commits one effect |
9
+ | Admission remains unavailable through the transport retry budget | Four failed acceptance attempts, three confirmed retry transfers, then transport DLQ; no receipt and no business effect were created |
10
+ | Invalid JSON or conflicting content for an existing message identity | Both are rejected to transport DLQ with `x-death.reason=rejected`; no additional business effect is committed |
11
+ | Accepted and ACKed messages fail inside the handler, then broker and Inbox service restart | Persisted Inbox retry state survives; transient failure completes on attempt 2; poison message reaches Inbox `DEAD` on attempt 3; failed attempts roll back their business writes |
12
+
13
+ The final database contains five completed receipts and one DEAD receipt, with no pending, processing, or retryable records. The main, retry, and dead-letter queues are empty after their results are inspected and acknowledged. Business-effect rows have independent UUID primary keys, so duplicate handler commits cannot be hidden by a per-receipt unique constraint.
14
+
15
+ ## Reproduce
16
+
17
+ Install the repository and example dependencies, then run:
18
+
19
+ ```sh
20
+ npm run build
21
+ node scripts/with-test-db.mjs node scripts/test-broker-recovery.mjs
22
+ ```
23
+
24
+ The script uses `amqplib` from `examples/node_modules`. The database wrapper ignores application database URLs and creates a random loopback-only PostgreSQL fixture. The broker harness creates its own random loopback-only RabbitMQ container and named volume, and removes both in `finally`. It re-reads Docker's published port after each restart because the ephemeral host port can change. It never restarts an existing broker.
25
+
26
+ ## Scope
27
+
28
+ This is a single-node RabbitMQ classic-queue test with durable queues, persistent messages, publisher confirms, and explicit manual ACKs. It verifies the application's acceptance/ACK and retry routing contract against a real broker. The harness drives each failure boundary explicitly instead of using an automatically reconnecting production consumer. Admission failure is real Inbox backpressure; this test does not substitute a simulated database outage for the separate network and database fault tests.
29
+
30
+ RabbitMQ cluster partitions, quorum-queue failover, production consumer reconnect policies, Kafka consumer-group rebalances, and AWS SQS remain deployment-specific validation. Transport DLQ and Inbox DEAD are separate states: the former contains messages that were never accepted, while the latter retains accepted messages whose handler exhausted its attempts.
@@ -0,0 +1,41 @@
1
+ # PostgreSQL failover and disk exhaustion tests
2
+
3
+ The [2026-10-28 local run](operations/2026-10-28-database.json) passed both scenarios with Node 24.22.1, Docker 29.9.2, and the `postgres:16-alpine` image. Standby promotion plus lease recovery took 1,979 ms; retry after freeing the full tablespace took 25 ms. All created containers and the private network were removed. These are single local observations, not recovery-time guarantees.
4
+
5
+ Run the isolated operational suite with a running Docker engine and installed repository dependencies:
6
+
7
+ ```sh
8
+ npm run test:operations:database
9
+ ```
10
+
11
+ The runner builds the current checkout and generates the fixture Prisma client. It ignores application `DATABASE_URL` values and creates its own random database, credentials, Docker network, and loopback-only ports. Every container is labeled and limited to one CPU, 768 MiB of memory, and a 512 MiB tmpfs PostgreSQL data directory. A separate 16 MiB tmpfs is used for the disk-full scenario. No host data directory is mounted and the test cannot fill the host filesystem.
12
+
13
+ Each command and readiness wait is bounded. The runner removes only the container IDs and network ID that it created, including on assertion failures. Its JSON report contains scenario results, timing, and cleanup status. Set `INBOX_DB_OPERATIONS_REPORT` to a new file path to preserve the same JSON; the runner refuses to overwrite an existing report.
14
+
15
+ ## Primary failure and standby promotion
16
+
17
+ The runner starts PostgreSQL 16 primary and standby servers. `pg_basebackup -R -X stream` initializes physical streaming replication. The test then creates three receipts:
18
+
19
+ - A completed receipt with committed business and outbox fixture rows.
20
+ - A claimed receipt whose handler has written business and outbox rows inside an uncommitted transaction.
21
+ - A pending receipt accepted while the other handler is paused.
22
+
23
+ Before killing the primary with `SIGKILL`, the test waits until the standby has replayed the primary's flushed WAL position covering the accepted receipts and claim. It verifies that the paused handler's uncommitted effects are invisible on the standby. The standby is then promoted using `pg_promote()`, and a new application-owned Prisma client connects to the promoted server.
24
+
25
+ The test checks that redelivery of all three messages remains a duplicate, the completed receipt is preserved, and the abandoned claim is recovered after its real lease expires. Each receipt must finish with exactly one business row and one outbox row. These fixture tables deliberately have no uniqueness constraint that could mask duplicate committed effects. The abandoned receipt must have two attempts.
26
+
27
+ This verifies recovery **after the tested WAL position has reached the standby**. Asynchronous replication can lose transactions that have not reached the failover target. The test does not claim zero data loss for arbitrary failover, automatic endpoint discovery, a high-availability control plane, or rejoining the old primary. The old primary stays stopped, preventing split brain. Applications and database infrastructure own replication durability, endpoint routing, fencing, and restoration policies.
28
+
29
+ ## Real PostgreSQL disk-full failure
30
+
31
+ A separate PostgreSQL instance has a tablespace on a **16 MiB tmpfs mount**. The test fills a ballast table with uncompressed one-MiB values until PostgreSQL returns SQLSTATE `53100` (`disk_full`). The insertion loop has a maximum of 32 attempts.
32
+
33
+ An Inbox handler writes its business and outbox fixture rows and then attempts to write to the exhausted tablespace in the same transaction. The test requires that processing becomes `RETRYABLE`, no business or outbox effect commits, and the failed tablespace write leaves no visible row. It then drops its disposable ballast table, truncates its disposable business table, retries the receipt, and verifies one committed set of effects, exactly two attempts, and duplicate handling on redelivery.
34
+
35
+ This exercises actual data-tablespace ENOSPC and recovery. WAL-volume, system-catalog-volume, inode exhaustion, cloud storage quotas, and a full host filesystem are different failure modes and are not covered here. The test intentionally leaves enough space for PostgreSQL and Inbox's retry-state storage to remain available.
36
+
37
+ ## References
38
+
39
+ - [PostgreSQL streaming replication and standby operation](https://www.postgresql.org/docs/16/warm-standby.html)
40
+ - [PostgreSQL pg_basebackup](https://www.postgresql.org/docs/16/app-pgbasebackup.html)
41
+ - [PostgreSQL error codes](https://www.postgresql.org/docs/16/errcodes-appendix.html)
@@ -6,6 +6,8 @@
6
6
 
7
7
  Preview.1 추가 범위는 [사용법](usage.md)의 완료 메시지 replay와 tenancy adapter, [예제](../examples/README.md)의 브로커·idempotency 연동을 따른다. 기존 AC12의 COMPLETED requeue 금지는 유지하며, 새 replay API는 원본을 보존한 별도 receipt를 생성한다.
8
8
 
9
+ 0.2.0 후속 작업에서는 [테스트 helper](testing.md), 실제 네트워크·프로세스 종료 복구, [standby 승격과 디스크 고갈](db-operations.md), [RabbitMQ 재시작·retry·DLQ](broker-recovery.md) 검증과 디스크 기반 지속 부하 측정 도구를 추가했다. 아래 단계와 초기 착수 목록은 원래 계획이며, 최신 실행 결과는 [검증 기록](validation.md)을 따른다. **M5 실제 서비스 도입·피드백은 이번 작업에서 제외했으며 완료하지 않았다.**
10
+
9
11
  ## 단계별 산출물
10
12
 
11
13
  | 단계 | 기간 가정 | 산출물 | 다음 단계 진입 기준 |
@@ -0,0 +1,34 @@
1
+ {
2
+ "node": "v24.22.1",
3
+ "image": "postgres:16-alpine",
4
+ "startedAt": "2026-10-28T13:17:04.937Z",
5
+ "scenarios": [
6
+ {
7
+ "scenario": "streaming-standby-promotion",
8
+ "passed": true,
9
+ "acceptedReceipts": 3,
10
+ "committedBusinessEffects": 3,
11
+ "committedOutboxEffects": 3,
12
+ "recoveryMs": 1979,
13
+ "replayedBeforePrimaryKill": "0/30070D8",
14
+ "replicationBoundary": "explicitly awaited standby replay before primary SIGKILL; not a claim of zero-loss asynchronous replication"
15
+ },
16
+ {
17
+ "scenario": "tablespace-enospc-recovery",
18
+ "passed": true,
19
+ "filesystemLimitBytes": 16777216,
20
+ "fillerRows": 15,
21
+ "postgresSqlstate": "53100",
22
+ "failedAttemptEffects": 0,
23
+ "committedBusinessEffects": 1,
24
+ "committedOutboxEffects": 1,
25
+ "attempts": 2,
26
+ "recoveryMs": 25,
27
+ "scope": "business data tablespace ENOSPC; WAL/system-volume exhaustion is not covered"
28
+ }
29
+ ],
30
+ "docker": "29.9.2",
31
+ "passed": true,
32
+ "cleanedUp": true,
33
+ "finishedAt": "2026-10-28T13:17:11.779Z"
34
+ }
@@ -247,4 +247,4 @@ Preview 출시는 [구현 로드맵](implementation-roadmap.md)의 장애·원
247
247
 
248
248
  초기 피드백에서 확인할 것은 도입 소요 시간, 대체한 애플리케이션 코드, 처리 지연, DB 부하, 운영자의 복구 가능성이다. 단순 npm 다운로드보다 실제 적용 사례를 우선한다. v1.0은 상태 전이·identity·migration·지원 버전 정책이 안정되고 장애 복구를 검증한 뒤 판단한다.
249
249
 
250
- 후속 우선순위는 검증된 브로커 예제 추가, tenancy adapter, 테스트 helper 순이다. 별도 public reliability-core와 다중 ORM은 실제로 중복되는 계약과 사용자 수요가 확인된 뒤 결정한다.
250
+ 초기 후속 우선순위였던 브로커 예제와 tenancy adapter는 0.1.0에, [테스트 helper](testing.md)는 0.2.0에 포함했다. 실제 서비스 2곳 도입·피드백은 이번 후속 작업에서 제외했으며 아직 목표로 남는다. 별도 public reliability-core와 다중 ORM은 실제로 중복되는 계약과 사용자 수요가 확인된 뒤 결정한다.
package/docs/releasing.md CHANGED
@@ -53,7 +53,15 @@ CI와 Release는 `release-validation.yml`을 공유한다. Node 22에서 한 번
53
53
 
54
54
  Node 22/24 × PostgreSQL 16/18의 네 job이 같은 tarball을 내려받아 실제 DB 통합/복구 테스트와 독립 consumer 설치를 실행한다. Node 24/PostgreSQL 16에서는 tarball 내부 예제 8종을 실제 임시 브로커와 함께 검사한다. 각 consumer는 복사본을 읽기 전용으로 사용하며 실행 전후 원본·복사본·metadata 변경을 검사한다.
55
55
 
56
- 네 조합이 모두 성공해야 배포 job이 실행된다. 배포 job에서는 의존성 설치·빌드·재패킹을 하지 않고 해당 tarball을 `--ignore-scripts --provenance`로 게시한다. 게시 후 레지스트리의 integrity가 검증한 tarball과 같은지 확인한다.
56
+ 네 조합과 별도 Node 24/PostgreSQL 16 운영 복구 job이 모두 성공해야 배포 job이 실행된다. 운영 job은 같은 커밋을 빌드하여 TCP 단절·blackhole, 연결 풀 포화, SIGTERM 종료, 실제 standby 승격, 제한된 tablespace 공간 고갈, RabbitMQ 재시작·retry·DLQ를 확인한다. 지속 부하 benchmark는 별도 수동 측정이며 CI의 성능 합격 기준으로 사용하지 않는다. 배포 job에서는 의존성 설치·빌드·재패킹을 하지 않고 해당 tarball을 `--ignore-scripts --provenance`로 게시한다. 게시 후 레지스트리의 integrity가 검증한 tarball과 같은지 확인한다.
57
+
58
+ npm은 게시 요청을 받은 뒤 설치 가능 상태가 되기 전에 패키지를 검사한다. 공식 [게시 시점 검사 안내](https://github.blog/changelog/2026-07-28-npm-publish-time-malware-scanning-and-dual-use-metadata/)에 따르면 보통 약 5분, 혼잡하거나 추가 검토가 필요하면 15분 이상 걸릴 수 있다. 게시 성공 직후의 404만으로 실패나 미게시를 판단하지 않는다. 워크플로우는 30초 간격으로 진행 상황을 출력하며 최대 20분 동안 공개 여부와 integrity를 확인하고, 배포 job에는 총 30분을 허용한다. 내용 불일치와 404 이외의 조회 오류는 즉시 실패한다.
59
+
60
+ 확인 시간이 초과되어도 같은 버전을 다시 게시하거나 전체 publish job을 바로 재실행하지 않는다. npm의 검사 상태·알림을 확인하고, 원래 검증한 `package.tgz`와 `metadata.json`을 보존한 채 확인 명령만 다시 실행한다. 검사에 더 오래 걸리거나 수동 검토가 필요한 경우 이 제한 안에 공개되지 않을 수 있다.
61
+
62
+ ```sh
63
+ node scripts/release-artifact.mjs verify-published "$INBOX_RELEASE_TARBALL" "$INBOX_RELEASE_METADATA"
64
+ ```
57
65
 
58
66
  같은 버전이 이미 존재하면 bytes가 같은 경우만 게시를 건너뛴다. 다른 bytes이거나 레지스트리 조회가 404 이외의 오류이면 실패한다. 기존 버전을 덮어쓰지 말고 원인을 확인한 다음 새 버전을 사용한다. 같은 버전의 재실행은 기존 dist-tag를 이동시키지 않는다.
59
67
 
@@ -0,0 +1,86 @@
1
+ # Testing application handlers
2
+
3
+ `@nestarc/inbox/testing` provides bounded helpers for integration tests using a real `InboxService` and the application's PostgreSQL test database. It has no additional runtime dependencies and works with any test runner. Apply the Inbox SQL schema through your test fixture before creating the service.
4
+
5
+ ## Process specific receipts
6
+
7
+ Disable automatic polling to control when processing starts. Register your application's real handler and pass the admin view for its trusted tenant scope:
8
+
9
+ ```ts
10
+ import { InboxService } from "@nestarc/inbox";
11
+ import { drainInbox } from "@nestarc/inbox/testing";
12
+
13
+ const inbox = new InboxService({
14
+ prisma,
15
+ handlers: [orderProjectionHandler],
16
+ worker: {
17
+ enabled: false,
18
+ retryBaseMs: 10,
19
+ retryMaxMs: 20,
20
+ },
21
+ });
22
+
23
+ try {
24
+ const receipt = await inbox.accept({
25
+ scope: { kind: "tenant", id: "tenant-a" },
26
+ source: "orders",
27
+ consumer: orderProjectionHandler.consumer,
28
+ messageId: "order-event-1",
29
+ eventType: orderProjectionHandler.eventType,
30
+ schemaVersion: orderProjectionHandler.schemaVersion,
31
+ payload: { orderId: "order-1" },
32
+ });
33
+
34
+ const [completed] = await drainInbox(
35
+ inbox,
36
+ inbox.admin.forTenant("tenant-a"),
37
+ [receipt.id],
38
+ { timeoutMs: 5_000 },
39
+ );
40
+
41
+ // completed.status is COMPLETED. Assert your actual business-table result too.
42
+ // A repeat delivery should return the same receipt and leave that result unchanged.
43
+ } finally {
44
+ await inbox.close();
45
+ // Disconnect the application's Prisma client after the worker has stopped.
46
+ }
47
+ ```
48
+
49
+ `drainInbox(inbox, admin, receiptIds, options)` checks each requested receipt and calls the real `runOnce()` between state checks until every receipt is `COMPLETED`. A zero-claim poll does not finish the helper: the message may be waiting for a retry or lease expiry. Retries use the worker's real schedule and do not advance a fake database clock. Configure short retry delays explicitly in tests when appropriate.
50
+
51
+ Receipt IDs select the completion assertions, **not the worker's claims**. `runOnce()` may process other receipts for its registered consumers, including other tenants. The scoped admin view restricts reads; it does not change worker ownership. Use an isolated database or dedicated test consumers, and avoid other workers when testing deterministic ordering. Receipt IDs are deduplicated, results preserve their first occurrence order, and an empty list returns immediately. A call accepts at most 1,000 IDs.
52
+
53
+ ## Observe an existing worker
54
+
55
+ `waitForInboxRecord()` only reads a receipt; it never starts processing, requeues a failure or changes the database. Use it with automatic polling or a separately managed worker:
56
+
57
+ ```ts
58
+ import { waitForInboxRecord } from "@nestarc/inbox/testing";
59
+
60
+ const completed = await waitForInboxRecord(admin, receipt.id);
61
+
62
+ // For a test deliberately exhausting retries:
63
+ const dead = await waitForInboxRecord(admin, failedReceipt.id, {
64
+ status: "DEAD",
65
+ timeoutMs: 10_000,
66
+ });
67
+ ```
68
+
69
+ The expected status is `COMPLETED` by default, or `DEAD` when specified. Observing the other terminal state fails immediately. A missing or out-of-scope record remains pending until the timeout or pass limit; this helper cannot distinguish those two cases.
70
+
71
+ ## Limits and failures
72
+
73
+ Both helpers accept these options:
74
+
75
+ | Option | Default | Meaning |
76
+ | ---------------- | ------- | --------------------------------------------------------------------------- |
77
+ | `timeoutMs` | `5000` | Overall budget including database reads and worker calls |
78
+ | `pollIntervalMs` | `10` | Delay between state-check passes |
79
+ | `maxPasses` | `1000` | Maximum state-check passes; no new worker call starts after the final check |
80
+ | `signal` | None | An `AbortSignal` that stops waiting and starting new polls |
81
+
82
+ Numeric options must be positive 32-bit integers. A stalled `get()` or `runOnce()` is also covered by the overall timeout. Timeout or cancellation stops the helper's wait; an already running database operation or handler may still finish. Always close the service before disconnecting Prisma or tearing down the fixture. These helpers do not create or reset databases and do not replace the runtime's transaction timeouts.
83
+
84
+ `InboxTestError` exposes `code` and a `records` snapshot containing only receipt IDs, last observed states and attempt counts. Codes are `TIMEOUT`, `ABORTED`, `PASS_LIMIT`, `UNEXPECTED_TERMINAL_STATE` and `INVALID_OPTIONS`. The helper's diagnostics omit payloads, tenant values and raw handler errors. Underlying database errors propagate unchanged so a broken database cannot be mistaken for an unfinished receipt.
85
+
86
+ Use real timers for PostgreSQL integration tests: advancing a test runner's fake timers does not advance PostgreSQL retry or lease timestamps. A successful wait proves that the requested receipts reached the expected state; assert application rows and outbox results separately to verify business effects.
package/docs/usage.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # @nestarc/inbox usage
2
2
 
3
- This document describes the 0.1.0 API. Inbox durably receives messages and processes them with Prisma and PostgreSQL. The application owns authentication, broker acknowledgments, its Prisma client, and migration deployment. APIs and schema may change before 1.0; review the [changelog](../CHANGELOG.md) before upgrading.
3
+ This document describes the 0.2.0 API. Inbox durably receives messages and processes them with Prisma and PostgreSQL. The application owns authentication, broker acknowledgments, its Prisma client, and migration deployment. APIs and schema may change before 1.0; review the [changelog](../CHANGELOG.md) before upgrading. For application integration tests, see the [bounded testing helpers](testing.md).
4
4
 
5
- Install with `npm install @nestarc/inbox@0.1.0`. To evaluate a source build, run `npm ci` and `npm pack` in the repository, then install the resulting `nestarc-inbox-0.1.0.tgz` in your application.
5
+ Install with `npm install @nestarc/inbox@0.2.0`. To evaluate a source build, run `npm ci` and `npm pack` in the repository, then install the resulting `nestarc-inbox-0.2.0.tgz` in your application.
6
6
 
7
7
  ## Initialize the schema and Prisma client
8
8
 
9
9
  Apply the SQL exported as `@nestarc/inbox/sql/create-inbox-table.sql` through your application's migration process before starting Inbox. Runtime initialization checks the schema; it does not create or upgrade tables. Keep the inbox SQL tables in your migration history and do not let Prisma schema synchronization remove unmanaged tables.
10
10
 
11
- Version 0.1.0 requires schema version 2. If you used schema v1 in a local development build, stop those processes, apply the exported `@nestarc/inbox/sql/migrate-v1-to-v2.sql`, then start 0.1.0 workers. The transactional migration preserves receipt identity, payloads, state, attempts, and tokens and may be applied again safely. It takes a table lock, so schedule the upgrade appropriately. Fresh installs use the current creation SQL. Old workers must not be restarted against v2. Local Preview.1 builds already use schema v2; the 0.1.0 promotion adds no schema change.
11
+ Versions 0.1.0 and 0.2.0 require schema version 2; upgrading from 0.1.0 needs no SQL migration. If you used schema v1 in a local development build, stop those processes, apply the exported `@nestarc/inbox/sql/migrate-v1-to-v2.sql`, then start 0.2.0 workers. The transactional migration preserves receipt identity, payloads, state, attempts, and tokens and may be applied again safely. It takes a table lock, so schedule the upgrade appropriately. Fresh installs use the current creation SQL. Old workers must not be restarted against v2. Local Preview.1 builds already use schema v2; the 0.1.0 promotion adds no schema change.
12
12
 
13
13
  Prisma 7 requires a driver adapter. Construct one application-owned client and pass it to Inbox:
14
14
 
@@ -1,5 +1,40 @@
1
1
  # @nestarc/inbox MVP 검증 기록
2
2
 
3
+ ## 0.2.0 후속 로컬 검증 — 2026년 10월 28일
4
+
5
+ 0.1.0 이후 테스트 helper와 운영 복구 검증을 추가했다. SQL schema v2와 기존 처리 계약은 유지한다. 이 절은 실제 실행한 로컬 검증이며, 아래 과거 기록의 미확인 항목 중 이 결과가 다루는 범위를 갱신한다. **실제 서비스 2곳 도입과 피드백(M5)은 요청에 따라 이번 작업에서 제외했으며 완료하지 않았다.**
6
+
7
+ | 검사 | 결과와 조건 |
8
+ | --- | --- |
9
+ | 빌드·타입·단위 테스트 | Node 24.22.1, 단위 105개 통과. 새 helper 단위 20개 포함 |
10
+ | 테스트 helper DB 검증 | PostgreSQL 16, 실제 retry→완료·DEAD 관측·scope/중복 수신 3개 통과 |
11
+ | 독립 tarball 소비자 | 0.2.0을 별도 앱에 설치, strict TypeScript·CJS·ESM과 testing subpath, 실제 rollback→retry→완료 통과 |
12
+ | 릴리즈 검증 | 46개 통과, 새 testing export와 필수 배포 파일 검사 포함 |
13
+ | TCP disconnect·blackhole | PostgreSQL 16/18에서 transaction 롤백과 같은 Prisma client의 재연결 후 단일 업무/outbox 효과 확인 |
14
+ | pool 포화 | 두 연결을 약 2.5초 점유하여 claim·수신 실패를 확인하고 해제 후 재전달·처리 회복 |
15
+ | SIGTERM | 실제 자식 프로세스의 약 2.5초 정상 drain과 700ms 제한의 멈춘 handler 중단·후속 worker 복구 |
16
+ | PostgreSQL failover | 실제 physical standby의 WAL replay 확인 후 primary SIGKILL·승격, receipt 3건의 중복 방지와 미완료 claim 회복 |
17
+ | 디스크 공간 부족 | 16 MiB 업무 tablespace에서 실제 SQLSTATE `53100`, 업무/outbox 롤백, 공간 확보 후 retry 성공 |
18
+ | RabbitMQ 복구 | 실제 재시작 3회, 미ACK 재전달·영속 retry/TTL·retry 소진·DLQ 등 5개 시나리오 통과 |
19
+
20
+ 네트워크·pool·SIGTERM 테스트는 [test/operations](../test/operations)에 있다. blackhole 시험은 driver `query_timeout=500ms`, `connectionTimeoutMillis=300ms`를 명시했다. 네트워크가 무기한 멈춰도 모든 기본 설정이 같은 시간 안에 복구한다는 보장은 아니다. PostgreSQL 18에서 이 다섯 테스트를 함께 실행한 시간은 약 10초였다.
21
+
22
+ [DB 운영 검증](db-operations.md)은 standby에 도착한 WAL을 기준으로 새 endpoint에 연결하는 절차를 검사한다. 비동기 복제에서 아직 전달되지 않은 commit의 무손실, 자동 장애 조치 시스템, WAL 볼륨 고갈을 보장하지 않는다. [브로커 복구](broker-recovery.md)는 RabbitMQ 단일 classic queue이며 Kafka rebalance·AWS SQS 운영 구성·브로커 클러스터 failover는 별도 배포 검증 대상이다.
23
+
24
+ `npm run test:operations`, `npm run test:operations:database`, `npm run test:broker-recovery`로 재현한다. DB와 브로커는 임의 이름·loopback 주소의 일회용 Docker fixture를 만들고 종료 시 제거한다. 일반 애플리케이션의 `DATABASE_URL`은 사용하지 않는다. CI와 Release의 별도 Node 24/PostgreSQL 16 job에서도 이 세 검증을 실행한다.
25
+
26
+ [디스크 기반 지속 부하 측정](benchmarks/2026-10-28-sustained-local.md)은 수신과 worker를 동시에 실행한 두 표본을 보존한다. 각 실행에서 120초 동안 19,199건을 수신했고 의도적 retry 385건을 거쳐 같은 수의 업무 효과와 완료 기록을 확인했다. 누락·중복 효과·예상 밖 오류는 0건이었다. 최대 backlog는 7,613/7,750건, 수신 종료 후 회복은 94.675/94.191초, 수신 p95는 20.615/21.736ms였다. 공유 pool 대기와 hot/cold tenant 지연을 기록했고 payload 24,199건 정리 뒤 fingerprint 보존·중복 검사 40/40을 확인했다. 각 표본은 약 214초이며 수시간·수일 soak test나 운영 처리량 보장은 아니다. 컨테이너와 전용 디스크 볼륨 정리를 확인했다.
27
+
28
+ ## 0.1.0 최초 npm 게시 — 2026년 10월 28일
29
+
30
+ 커밋 `15b01813f009bb012f88a081bdd4a7705befc90e`의 [CI 실행 40686580892](https://github.com/nestarc/inbox/actions/runs/40686580892)이 전체 성공했다. Node 22/24 × PostgreSQL 16/18 네 조합과 예제 8종이 검증한 동일 tarball을 대화형 2FA 인증으로 최초 게시했다. npm의 검사 대기 후 공개 레지스트리의 `@nestarc/inbox@0.1.0`과 `latest: 0.1.0`을 확인했고, 실제 내려받은 tarball의 SHA-512가 CI 산출물과 일치했다. 배포물은 87개 파일, 105,710바이트이며 SHA-256은 `9bd1b4292cb62a72fdff0d1b7fc4cf01a5f7a4967003022b28dd0f4d6f192c62`다.
31
+
32
+ [GitHub Release v0.1.0](https://github.com/nestarc/inbox/releases/tag/v0.1.0)은 같은 커밋을 가리킨다. npm Trusted Publisher는 `nestarc/inbox`, `release.yml`, environment `npm`, 직접 publish 허용으로 등록했다. 최초 수동 게시에는 GitHub OIDC provenance가 없다. 등록 성공과 실제 OIDC 게시 성공은 별개이며, 동일 버전의 무결성 확인·게시 건너뛰기로는 OIDC 인증을 검증할 수 없다. 새 연결은 [npm의 활성화 기한](https://docs.npmjs.com/trusted-publishers/)인 2일 안에 첫 게시를 성공시켜야 하며, 다음 새 버전으로 검증해야 한다.
33
+
34
+ 게시 직후 HTTP 202 성공 응답 뒤 공개 조회가 잠시 404를 반환하는 상황을 관찰했다. 이후 배포 도구의 확인 대기를 16초에서 최대 20분으로 보완하고, 가상 시간으로 지연 공개·시간 초과·내용 불일치·조회 오류를 검사하는 테스트 5개를 추가했다. 릴리즈 테스트는 총 46개 통과했고 actionlint와 포맷 검사도 통과했다. 이 자동화 보완은 이미 게시한 0.1.0 tarball에 소급 반영하지 않는다.
35
+
36
+ 아래 기록의 최초 게시·연결 미완료 표시는 각 검사 당시 상태를 보존한 이력이다.
37
+
3
38
  ## GitHub hosted CI 검증 — 2026년 10월 28일
4
39
 
5
40
  커밋 `664b4fc`의 [CI 실행 40684612217](https://github.com/nestarc/inbox/actions/runs/40684612217)이 전체 성공했다. GitHub hosted Linux runner에서 Node 22/24 × PostgreSQL 16/18 네 조합의 단위·통합/복구·동일 tarball 소비자 검증을 통과했고, Node 24/PostgreSQL 16에서 브로커를 포함한 예제 8종도 통과했다. 빌드·타입·포맷·릴리즈 검증과 artifact 무결성 확인도 성공했다.
@@ -9,7 +9,7 @@ Use Node 22.23.4+ or 24.22.1+ for the full examples project because it includes
9
9
  Run these commands from `examples/`:
10
10
 
11
11
  ```sh
12
- npm install --no-save @nestarc/inbox@0.1.0
12
+ npm install --no-save @nestarc/inbox@0.2.0
13
13
  npm run generate
14
14
  npm run build
15
15
  ```
@@ -17,12 +17,12 @@ npm run build
17
17
  To run against a local source build instead, run `npm ci` and `npm pack` from the repository root. Then, from `examples/`, install the tarball and regenerate/build:
18
18
 
19
19
  ```sh
20
- npm install --no-save ../nestarc-inbox-0.1.0.tgz
20
+ npm install --no-save ../nestarc-inbox-0.2.0.tgz
21
21
  npm run generate
22
22
  npm run build
23
23
  ```
24
24
 
25
- The automated test runners always install the tarball built from the checkout, so they exercise the exact candidate artifact. Prisma 7.10.1 uses the PostgreSQL driver adapter and the generated client in `src/generated/prisma`. Version 0.1.0 uses schema v2 and the runtime implementation validated under the local Preview.1 development version.
25
+ The automated test runners always install the tarball built from the checkout, so they exercise the exact candidate artifact. Prisma 7.10.1 uses the PostgreSQL driver adapter and the generated client in `src/generated/prisma`. Version 0.2.0 retains schema v2 and the processing runtime first published in 0.1.0, and adds the optional testing-helper subpath.
26
26
 
27
27
  ## Start a dedicated database
28
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestarc/inbox",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Transactional inbox for NestJS, Prisma and PostgreSQL with durable deduplication, fenced processing and retries.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,6 +16,11 @@
16
16
  "require": "./dist/index.js",
17
17
  "default": "./dist/index.js"
18
18
  },
19
+ "./testing": {
20
+ "types": "./dist/testing/index.d.ts",
21
+ "require": "./dist/testing/index.js",
22
+ "default": "./dist/testing/index.js"
23
+ },
19
24
  "./sql/create-inbox-table.sql": "./src/sql/create-inbox-table.sql",
20
25
  "./sql/migrate-v1-to-v2.sql": "./src/sql/migrate-v1-to-v2.sql"
21
26
  },
@@ -43,12 +48,16 @@
43
48
  "test:release": "node --test test/release/*.test.mjs",
44
49
  "test:integration": "node scripts/with-test-db.mjs npm run test:integration:run",
45
50
  "test:integration:run": "vitest run test/integration test/failure",
51
+ "test:operations": "node scripts/with-test-db.mjs node node_modules/vitest/vitest.mjs run test/operations",
52
+ "test:operations:database": "node scripts/test-db-operations.mjs",
53
+ "test:broker-recovery": "node scripts/with-test-db.mjs node scripts/test-broker-recovery.mjs",
46
54
  "test:package": "node scripts/with-test-db.mjs node scripts/test-package.mjs",
47
55
  "test:examples": "node scripts/with-test-db.mjs node scripts/test-examples.mjs",
48
56
  "prebenchmark": "npm run build",
49
57
  "benchmark": "node scripts/with-test-db.mjs node scripts/benchmark.mjs",
50
- "format": "prettier --write src test scripts .github/workflows README.md CHANGELOG.md docs/usage.md docs/releasing.md package.json tsconfig*.json vitest.config.mts",
51
- "format:check": "prettier --check src test scripts .github/workflows README.md CHANGELOG.md docs/usage.md docs/releasing.md package.json tsconfig*.json vitest.config.mts",
58
+ "benchmark:sustained": "node scripts/with-benchmark-db.mjs",
59
+ "format": "prettier --write src test scripts .github/workflows README.md CHANGELOG.md docs/usage.md docs/releasing.md docs/testing.md package.json tsconfig*.json vitest.config.mts",
60
+ "format:check": "prettier --check src test scripts .github/workflows README.md CHANGELOG.md docs/usage.md docs/releasing.md docs/testing.md package.json tsconfig*.json vitest.config.mts",
52
61
  "prepack": "npm run build"
53
62
  },
54
63
  "peerDependencies": {