@nestarc/inbox 0.0.0-stage → 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.
- package/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +309 -2
- package/dist/contracts/errors.d.ts +34 -0
- package/dist/contracts/errors.js +73 -0
- package/dist/contracts/errors.js.map +1 -0
- package/dist/contracts/types.d.ts +174 -0
- package/dist/contracts/types.js +3 -0
- package/dist/contracts/types.js.map +1 -0
- package/dist/inbox-admin.service.d.ts +44 -0
- package/dist/inbox-admin.service.js +102 -0
- package/dist/inbox-admin.service.js.map +1 -0
- package/dist/inbox.module.d.ts +13 -0
- package/dist/inbox.module.js +59 -0
- package/dist/inbox.module.js.map +1 -0
- package/dist/inbox.service.d.ts +22 -0
- package/dist/inbox.service.js +74 -0
- package/dist/inbox.service.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/tenancy.d.ts +25 -0
- package/dist/integrations/tenancy.js +56 -0
- package/dist/integrations/tenancy.js.map +1 -0
- package/dist/internal/handler-context.d.ts +3 -0
- package/dist/internal/handler-context.js +39 -0
- package/dist/internal/handler-context.js.map +1 -0
- package/dist/internal/options.d.ts +5 -0
- package/dist/internal/options.js +86 -0
- package/dist/internal/options.js.map +1 -0
- package/dist/internal/reliability-core/abort.d.ts +2 -0
- package/dist/internal/reliability-core/abort.js +21 -0
- package/dist/internal/reliability-core/abort.js.map +1 -0
- package/dist/internal/reliability-core/index.d.ts +3 -0
- package/dist/internal/reliability-core/index.js +14 -0
- package/dist/internal/reliability-core/index.js.map +1 -0
- package/dist/internal/reliability-core/json.d.ts +13 -0
- package/dist/internal/reliability-core/json.js +125 -0
- package/dist/internal/reliability-core/json.js.map +1 -0
- package/dist/internal/reliability-core/retry.d.ts +8 -0
- package/dist/internal/reliability-core/retry.js +26 -0
- package/dist/internal/reliability-core/retry.js.map +1 -0
- package/dist/internal/validation.d.ts +6 -0
- package/dist/internal/validation.js +123 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/storage/prisma/prisma-inbox.storage.d.ts +30 -0
- package/dist/storage/prisma/prisma-inbox.storage.js +369 -0
- package/dist/storage/prisma/prisma-inbox.storage.js.map +1 -0
- package/dist/testing/index.d.ts +40 -0
- package/dist/testing/index.js +120 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/worker/inbox-worker.d.ts +31 -0
- package/dist/worker/inbox-worker.js +343 -0
- package/dist/worker/inbox-worker.js.map +1 -0
- package/docs/adr/0001-durable-inbox.md +40 -0
- package/docs/benchmarks/2026-10-27-local.md +31 -0
- package/docs/benchmarks/2026-10-28-preview1-local.md +87 -0
- package/docs/benchmarks/2026-10-28-preview1-run1.json +120 -0
- package/docs/benchmarks/2026-10-28-preview1-run2.json +120 -0
- package/docs/benchmarks/2026-10-28-preview1-run3.json +120 -0
- package/docs/benchmarks/2026-10-28-sustained-local.md +104 -0
- package/docs/benchmarks/2026-10-28-sustained-run1.json +2553 -0
- package/docs/benchmarks/2026-10-28-sustained-run2.json +2553 -0
- package/docs/broker-recovery-results.json +52 -0
- package/docs/broker-recovery.md +30 -0
- package/docs/db-operations.md +41 -0
- package/docs/implementation-roadmap.md +129 -0
- package/docs/operations/2026-10-28-database.json +34 -0
- package/docs/package-plan.md +250 -0
- package/docs/releasing.md +91 -0
- package/docs/testing.md +86 -0
- package/docs/usage.md +266 -0
- package/docs/validation.md +174 -0
- package/examples/README.md +124 -0
- package/examples/compose.yaml +26 -0
- package/examples/package.json +42 -0
- package/examples/prisma/schema.prisma +29 -0
- package/examples/prisma.config.ts +10 -0
- package/examples/scripts/prepare-db.cjs +45 -0
- package/examples/scripts/smoke.cjs +37 -0
- package/examples/scripts/with-brokers.cjs +249 -0
- package/examples/scripts/with-rabbitmq.cjs +131 -0
- package/examples/sql/application.sql +14 -0
- package/examples/sql/tenancy.sql +26 -0
- package/examples/src/broker-common.ts +79 -0
- package/examples/src/bullmq-ingress/main.ts +165 -0
- package/examples/src/common.ts +112 -0
- package/examples/src/idempotency-ingress/main.ts +211 -0
- package/examples/src/kafka-ingress/main.ts +209 -0
- package/examples/src/outbox-to-inbox/main.ts +198 -0
- package/examples/src/rabbitmq-ingress/main.ts +203 -0
- package/examples/src/sqs-ingress/main.ts +273 -0
- package/examples/src/tenancy/main.ts +210 -0
- package/examples/src/webhook-ingress/main.ts +172 -0
- package/examples/tsconfig.json +16 -0
- package/package.json +99 -4
- package/src/sql/create-inbox-table.sql +100 -0
- package/src/sql/migrate-v1-to-v2.sql +53 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "nestarc-inbox-local-baseline",
|
|
3
|
+
"measuredAt": "2026-10-28T11:49:36.479Z",
|
|
4
|
+
"packageVersion": "0.1.0-preview.1",
|
|
5
|
+
"metadata": {
|
|
6
|
+
"node": "v24.22.1",
|
|
7
|
+
"platform": "darwin",
|
|
8
|
+
"arch": "arm64",
|
|
9
|
+
"cpuModel": "Apple M1 Pro",
|
|
10
|
+
"hostLogicalCpus": 10,
|
|
11
|
+
"postgres": "PostgreSQL 16.15 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit",
|
|
12
|
+
"image": "postgres:16-alpine",
|
|
13
|
+
"storage": "Docker tmpfs; loopback forwarded TCP",
|
|
14
|
+
"prisma": "7.10.1",
|
|
15
|
+
"connectionMax": 12,
|
|
16
|
+
"fixtureAdminConnectionMax": 2,
|
|
17
|
+
"acceptConcurrency": 8,
|
|
18
|
+
"workerConcurrency": 4,
|
|
19
|
+
"iterationsPerScenario": 500,
|
|
20
|
+
"payloadBytes": 265,
|
|
21
|
+
"retainedPayloads": "purged tombstones",
|
|
22
|
+
"warmup": "SELECT 1 at accept concurrency before each scenario; no warmup inbox writes",
|
|
23
|
+
"workerEffect": "One INSERT through the same Prisma transaction as inbox completion"
|
|
24
|
+
},
|
|
25
|
+
"scenarios": [
|
|
26
|
+
{
|
|
27
|
+
"name": "fresh_unique",
|
|
28
|
+
"retainedCompletedReceipts": 0,
|
|
29
|
+
"iterations": 500,
|
|
30
|
+
"expectedUniqueMessages": 500,
|
|
31
|
+
"duplicateRatio": 0,
|
|
32
|
+
"accepted": 500,
|
|
33
|
+
"duplicates": 0,
|
|
34
|
+
"accept": {
|
|
35
|
+
"elapsedMs": 236.124,
|
|
36
|
+
"requestsPerSecond": 2117.532,
|
|
37
|
+
"latencyMs": {
|
|
38
|
+
"p50": 3.471,
|
|
39
|
+
"p95": 5.406,
|
|
40
|
+
"p99": 9.947
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"worker": {
|
|
44
|
+
"drainMs": 928.155,
|
|
45
|
+
"completedPerSecond": 538.703,
|
|
46
|
+
"batches": 125,
|
|
47
|
+
"committedEffects": 500
|
|
48
|
+
},
|
|
49
|
+
"errors": {
|
|
50
|
+
"accept": {},
|
|
51
|
+
"worker": {}
|
|
52
|
+
},
|
|
53
|
+
"verified": true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "retained_10000_unique",
|
|
57
|
+
"retainedCompletedReceipts": 10000,
|
|
58
|
+
"iterations": 500,
|
|
59
|
+
"expectedUniqueMessages": 500,
|
|
60
|
+
"duplicateRatio": 0,
|
|
61
|
+
"accepted": 500,
|
|
62
|
+
"duplicates": 0,
|
|
63
|
+
"accept": {
|
|
64
|
+
"elapsedMs": 202.596,
|
|
65
|
+
"requestsPerSecond": 2467.967,
|
|
66
|
+
"latencyMs": {
|
|
67
|
+
"p50": 2.961,
|
|
68
|
+
"p95": 5.244,
|
|
69
|
+
"p99": 7.506
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"worker": {
|
|
73
|
+
"drainMs": 889.917,
|
|
74
|
+
"completedPerSecond": 561.85,
|
|
75
|
+
"batches": 125,
|
|
76
|
+
"committedEffects": 500
|
|
77
|
+
},
|
|
78
|
+
"errors": {
|
|
79
|
+
"accept": {},
|
|
80
|
+
"worker": {}
|
|
81
|
+
},
|
|
82
|
+
"verified": true
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "retained_10000_duplicates_90pct",
|
|
86
|
+
"retainedCompletedReceipts": 10000,
|
|
87
|
+
"iterations": 500,
|
|
88
|
+
"expectedUniqueMessages": 50,
|
|
89
|
+
"duplicateRatio": 0.9,
|
|
90
|
+
"accepted": 50,
|
|
91
|
+
"duplicates": 450,
|
|
92
|
+
"accept": {
|
|
93
|
+
"elapsedMs": 268.173,
|
|
94
|
+
"requestsPerSecond": 1864.466,
|
|
95
|
+
"latencyMs": {
|
|
96
|
+
"p50": 4.018,
|
|
97
|
+
"p95": 6.622,
|
|
98
|
+
"p99": 8.278
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"worker": {
|
|
102
|
+
"drainMs": 91.407,
|
|
103
|
+
"completedPerSecond": 547.005,
|
|
104
|
+
"batches": 13,
|
|
105
|
+
"committedEffects": 50
|
|
106
|
+
},
|
|
107
|
+
"errors": {
|
|
108
|
+
"accept": {},
|
|
109
|
+
"worker": {}
|
|
110
|
+
},
|
|
111
|
+
"verified": true
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"limitations": [
|
|
115
|
+
"One local sample on a disposable tmpfs database; no SLA or production capacity claim.",
|
|
116
|
+
"Scenarios run in a fixed order; later scenarios may benefit from JIT/cache warmup.",
|
|
117
|
+
"No sustained load, network faults, broker traffic, cold-cache runs, or fairness measurement.",
|
|
118
|
+
"Receipt seed and schema preparation are excluded from measurements."
|
|
119
|
+
]
|
|
120
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "nestarc-inbox-local-baseline",
|
|
3
|
+
"measuredAt": "2026-10-28T11:49:53.903Z",
|
|
4
|
+
"packageVersion": "0.1.0-preview.1",
|
|
5
|
+
"metadata": {
|
|
6
|
+
"node": "v24.22.1",
|
|
7
|
+
"platform": "darwin",
|
|
8
|
+
"arch": "arm64",
|
|
9
|
+
"cpuModel": "Apple M1 Pro",
|
|
10
|
+
"hostLogicalCpus": 10,
|
|
11
|
+
"postgres": "PostgreSQL 16.15 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit",
|
|
12
|
+
"image": "postgres:16-alpine",
|
|
13
|
+
"storage": "Docker tmpfs; loopback forwarded TCP",
|
|
14
|
+
"prisma": "7.10.1",
|
|
15
|
+
"connectionMax": 12,
|
|
16
|
+
"fixtureAdminConnectionMax": 2,
|
|
17
|
+
"acceptConcurrency": 8,
|
|
18
|
+
"workerConcurrency": 4,
|
|
19
|
+
"iterationsPerScenario": 500,
|
|
20
|
+
"payloadBytes": 265,
|
|
21
|
+
"retainedPayloads": "purged tombstones",
|
|
22
|
+
"warmup": "SELECT 1 at accept concurrency before each scenario; no warmup inbox writes",
|
|
23
|
+
"workerEffect": "One INSERT through the same Prisma transaction as inbox completion"
|
|
24
|
+
},
|
|
25
|
+
"scenarios": [
|
|
26
|
+
{
|
|
27
|
+
"name": "fresh_unique",
|
|
28
|
+
"retainedCompletedReceipts": 0,
|
|
29
|
+
"iterations": 500,
|
|
30
|
+
"expectedUniqueMessages": 500,
|
|
31
|
+
"duplicateRatio": 0,
|
|
32
|
+
"accepted": 500,
|
|
33
|
+
"duplicates": 0,
|
|
34
|
+
"accept": {
|
|
35
|
+
"elapsedMs": 357.016,
|
|
36
|
+
"requestsPerSecond": 1400.499,
|
|
37
|
+
"latencyMs": {
|
|
38
|
+
"p50": 3.903,
|
|
39
|
+
"p95": 11.612,
|
|
40
|
+
"p99": 37.17
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"worker": {
|
|
44
|
+
"drainMs": 921.193,
|
|
45
|
+
"completedPerSecond": 542.775,
|
|
46
|
+
"batches": 125,
|
|
47
|
+
"committedEffects": 500
|
|
48
|
+
},
|
|
49
|
+
"errors": {
|
|
50
|
+
"accept": {},
|
|
51
|
+
"worker": {}
|
|
52
|
+
},
|
|
53
|
+
"verified": true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "retained_10000_unique",
|
|
57
|
+
"retainedCompletedReceipts": 10000,
|
|
58
|
+
"iterations": 500,
|
|
59
|
+
"expectedUniqueMessages": 500,
|
|
60
|
+
"duplicateRatio": 0,
|
|
61
|
+
"accepted": 500,
|
|
62
|
+
"duplicates": 0,
|
|
63
|
+
"accept": {
|
|
64
|
+
"elapsedMs": 211.955,
|
|
65
|
+
"requestsPerSecond": 2358.995,
|
|
66
|
+
"latencyMs": {
|
|
67
|
+
"p50": 3.163,
|
|
68
|
+
"p95": 4.602,
|
|
69
|
+
"p99": 6.723
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"worker": {
|
|
73
|
+
"drainMs": 894.918,
|
|
74
|
+
"completedPerSecond": 558.711,
|
|
75
|
+
"batches": 125,
|
|
76
|
+
"committedEffects": 500
|
|
77
|
+
},
|
|
78
|
+
"errors": {
|
|
79
|
+
"accept": {},
|
|
80
|
+
"worker": {}
|
|
81
|
+
},
|
|
82
|
+
"verified": true
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "retained_10000_duplicates_90pct",
|
|
86
|
+
"retainedCompletedReceipts": 10000,
|
|
87
|
+
"iterations": 500,
|
|
88
|
+
"expectedUniqueMessages": 50,
|
|
89
|
+
"duplicateRatio": 0.9,
|
|
90
|
+
"accepted": 50,
|
|
91
|
+
"duplicates": 450,
|
|
92
|
+
"accept": {
|
|
93
|
+
"elapsedMs": 242.491,
|
|
94
|
+
"requestsPerSecond": 2061.929,
|
|
95
|
+
"latencyMs": {
|
|
96
|
+
"p50": 3.726,
|
|
97
|
+
"p95": 5.29,
|
|
98
|
+
"p99": 7.027
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"worker": {
|
|
102
|
+
"drainMs": 88.991,
|
|
103
|
+
"completedPerSecond": 561.857,
|
|
104
|
+
"batches": 13,
|
|
105
|
+
"committedEffects": 50
|
|
106
|
+
},
|
|
107
|
+
"errors": {
|
|
108
|
+
"accept": {},
|
|
109
|
+
"worker": {}
|
|
110
|
+
},
|
|
111
|
+
"verified": true
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"limitations": [
|
|
115
|
+
"One local sample on a disposable tmpfs database; no SLA or production capacity claim.",
|
|
116
|
+
"Scenarios run in a fixed order; later scenarios may benefit from JIT/cache warmup.",
|
|
117
|
+
"No sustained load, network faults, broker traffic, cold-cache runs, or fairness measurement.",
|
|
118
|
+
"Receipt seed and schema preparation are excluded from measurements."
|
|
119
|
+
]
|
|
120
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Sustained ingress, recovery, and retention on a disk-backed database
|
|
2
|
+
|
|
3
|
+
This report records two local runs of the `0.2.0` candidate. Each run uses a fresh PostgreSQL container and a dedicated Docker volume, with simultaneous receipt acceptance and worker processing. The runs exercise a bounded overload and recovery window; they are not a production capacity estimate or an hours-long soak test.
|
|
4
|
+
|
|
5
|
+
## Workload and environment
|
|
6
|
+
|
|
7
|
+
| Setting | Value |
|
|
8
|
+
| ----------------- | ---------------------------------------------------------------------------------------------- |
|
|
9
|
+
| Node / Prisma | 24.22.1 / 7.10.1 |
|
|
10
|
+
| PostgreSQL | 16.15, official `postgres:16-alpine` image |
|
|
11
|
+
| Host | macOS arm64, Apple M1 Pro, 10 logical CPUs |
|
|
12
|
+
| Docker | 29.9.2; database limited to 2 CPUs and 1 GiB |
|
|
13
|
+
| Database storage | Fresh named volume on the Docker VM disk; no PostgreSQL tmpfs |
|
|
14
|
+
| Durability | `fsync`, `synchronous_commit`, and `full_page_writes` enabled |
|
|
15
|
+
| Existing history | 5,000 completed receipts with retained 1 KiB payload bodies |
|
|
16
|
+
| Ingress | 120 seconds: 30 s at 80/s, 60 s at 240/s, 30 s at 80/s |
|
|
17
|
+
| Concurrency | At most 16 acceptance requests, 4 handler tasks |
|
|
18
|
+
| Worker polling | Continuous awaited `runOnce()` calls, with a 5 ms pause after an empty poll |
|
|
19
|
+
| Connections | Shared application pool of 8; separate measurement pool of 2 |
|
|
20
|
+
| Pool pressure | Hold 4 application connections from about second 48 to 78 |
|
|
21
|
+
| Tenants | One hot tenant receives 95%; one cold tenant receives 5% |
|
|
22
|
+
| Handler | 15 ms SQL sleep followed by one business insert in the inbox transaction |
|
|
23
|
+
| Retry | Every 50th message within each tenant fails after its first insert, forcing rollback and retry |
|
|
24
|
+
| Recovery deadline | 180 seconds after ingress stops |
|
|
25
|
+
| Retention | Purge completed payloads in batches of 500, then ordinary `VACUUM (ANALYZE)` |
|
|
26
|
+
|
|
27
|
+
The effect table has an independent sequence primary key. Inbox IDs and message IDs have no unique constraint in that table, so duplicate committed effects cannot be hidden by a business uniqueness check. Verification checks every accepted receipt for exactly one committed effect and a completed status, as well as the expected retries and zero unexpected errors.
|
|
28
|
+
|
|
29
|
+
Receipt payloads use 64 random 1 KiB bodies. The acceptance scheduler is bounded: when all 16 request slots are occupied, it waits rather than generating an unbounded client queue. The JSON reports distinguish target submissions, actual attempts, accepted receipts, and unsent target submissions. Sampled pool queue depth and measured connection acquisition time describe this shared pool; they do not measure a remote production pool.
|
|
30
|
+
|
|
31
|
+
## Results
|
|
32
|
+
|
|
33
|
+
Both runs passed. Across **38,398 accepted receipts**, the database contained exactly **38,398 completed receipts and business effects**, with zero missing or duplicate committed effects. The **770 intentional first-attempt failures** rolled back and retried successfully. There were no unexpected acceptance, worker, lease-loss, or DEAD outcomes.
|
|
34
|
+
|
|
35
|
+
| Measurement | Run 1 | Run 2 |
|
|
36
|
+
| ------------------------------------------------- | -------------------------: | -------------------------: |
|
|
37
|
+
| Accepted / completed / business effects | 19,199 / 19,199 / 19,199 | 19,199 / 19,199 / 19,199 |
|
|
38
|
+
| Intentional rollbacks and retries | 385 | 385 |
|
|
39
|
+
| Receive requests/s during the 120 s ingress phase | 159.992 | 159.991 |
|
|
40
|
+
| Acceptance p50 / p95 / p99 | 3.480 / 20.615 / 25.915 ms | 3.573 / 21.736 / 27.094 ms |
|
|
41
|
+
| Shared-pool acquisition p95 / p99 | 11.084 / 20.255 ms | 11.652 / 21.132 ms |
|
|
42
|
+
| One-second samples with queued pool acquisition | 25 | 22 |
|
|
43
|
+
| Highest sampled pool queue depth | 6 | 6 |
|
|
44
|
+
| Highest sampled backlog | 7,613 | 7,750 |
|
|
45
|
+
| Backlog when ingress stopped | 7,573 | 7,676 |
|
|
46
|
+
| Recovery after ingress stopped | 94.675 s | 94.191 s |
|
|
47
|
+
| Total ingress plus recovery | 214.675 s | 214.191 s |
|
|
48
|
+
| Cold tenant completed / completion p95 | 960 / 95.571 s | 960 / 95.478 s |
|
|
49
|
+
| Hot tenant completed / completion p95 | 18,239 / 95.574 s | 18,239 / 95.483 s |
|
|
50
|
+
| Missing / duplicate committed effects | 0 / 0 | 0 / 0 |
|
|
51
|
+
|
|
52
|
+
Both offered schedules had 19,200 boundary-inclusive target slots. The strict 120-second cutoff submitted 19,199 of them; the unsubmitted final slot is recorded in each JSON rather than counted as an accepted message. The approximate 160/s receipt rate is the configured mixed offered load, not a maximum-throughput finding. The 95-second completion p95 includes accumulated queue delay under the deliberate overload; it is distinct from the acceptance latency.
|
|
53
|
+
|
|
54
|
+
The original reports preserve every one-second sample, exact counters, environment details, and latency distributions:
|
|
55
|
+
|
|
56
|
+
- [Run 1](2026-10-28-sustained-run1.json): completed at `2026-10-28T13:21:12.858Z` / 22:21:12.858 KST.
|
|
57
|
+
- [Run 2](2026-10-28-sustained-run2.json): completed at `2026-10-28T13:25:04.040Z` / 22:25:04.040 KST.
|
|
58
|
+
|
|
59
|
+
Both used image ID `sha256:f4610795587492cd4fb887c2dc3de301b813278bb6f616992061141b5f15ac36`. Each runner exited successfully, and a final inspection found no remaining benchmark-labeled containers or volumes. Earlier 10-second harness development checks are not included in these measurements.
|
|
60
|
+
|
|
61
|
+
## Retention guidance
|
|
62
|
+
|
|
63
|
+
Payload cleanup preserves the receipt identity and fingerprint. The benchmark re-delivers 40 original envelopes after cleanup and checks that they remain duplicates with the original completed receipt and no new business effect. It also compares a checksum of every stored receipt ID and fingerprint before and after cleanup.
|
|
64
|
+
|
|
65
|
+
| Storage measurement | Run 1 | Run 2 |
|
|
66
|
+
| ----------------------------------------------------- | ----------------: | ----------------: |
|
|
67
|
+
| Seeded history: 5,000 receipts, total relation size | 7.98 MiB | 7.98 MiB |
|
|
68
|
+
| Completed receipts before cleanup | 24,199 | 24,199 |
|
|
69
|
+
| Logical payload storage before → after cleanup | 24.55 → 0 MiB | 24.55 → 0 MiB |
|
|
70
|
+
| Heap before cleanup | 44.68 MiB | 44.98 MiB |
|
|
71
|
+
| Indexes before → after cleanup | 9.75 → 10.31 MiB | 9.70 → 10.39 MiB |
|
|
72
|
+
| Total relation size before → after cleanup and vacuum | 54.48 → 55.04 MiB | 54.73 → 55.41 MiB |
|
|
73
|
+
| Cleanup time, 500-row batches | 723.099 ms | 715.353 ms |
|
|
74
|
+
| Receipt/fingerprint checksum preserved | Yes | Yes |
|
|
75
|
+
| Duplicate re-delivery probes after cleanup | 40 / 40 | 40 / 40 |
|
|
76
|
+
|
|
77
|
+
The growth includes receipt insertion, claim/completion updates, intentional retries, and index maintenance. It is not a stable bytes-per-message coefficient for other applications. Cleanup removed all logical payload bytes but left physical allocation available for reuse; the small index increase during cleanup is included rather than hidden.
|
|
78
|
+
|
|
79
|
+
Use bounded purge batches and measure table, index, and WAL growth in the application workload. Deleting a payload does not remove the deduplication identity or its indexes. Ordinary vacuum makes dead tuple space reusable and does not promise that allocated relation bytes shrink immediately. Retention capacity therefore needs to account for continuing receipt and index growth even when payloads are purged.
|
|
80
|
+
|
|
81
|
+
The hot/cold tenant measurements expose queue delay under shared scheduling. They do not establish a fairness bound. A tenant with low traffic can still wait behind accumulated work; measure the application's tail latency before choosing any additional scheduling policy.
|
|
82
|
+
|
|
83
|
+
## Reproduce
|
|
84
|
+
|
|
85
|
+
With Docker running and repository dependencies installed:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
npm run benchmark:sustained --silent > /tmp/inbox-sustained-run1.json
|
|
89
|
+
npm run benchmark:sustained --silent > /tmp/inbox-sustained-run2.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The runner builds the current checkout and generates the test Prisma client. Setup and progress go to stderr; stdout contains only the JSON report. It ignores an existing application `DATABASE_URL`, creates a randomly named database, binds its port only on loopback, and removes its own container and volume in cleanup. The database is capped at 2 CPUs and 1 GiB; commands and the benchmark child have bounded execution times.
|
|
93
|
+
|
|
94
|
+
For a short harness check, use `INBOX_BENCHMARK_SECONDS=10`. Allowed ingress durations are 10–300 seconds; `INBOX_BENCHMARK_RECOVERY_SECONDS` is also bounded to 10–300 seconds. `INBOX_BENCHMARK_RETAINED` controls the seeded history, from 0 to 50,000 receipts. A run exits unsuccessfully if correctness checks fail or the intended pool-queue/backlog conditions were not observed.
|
|
95
|
+
|
|
96
|
+
## Interpretation limits
|
|
97
|
+
|
|
98
|
+
- Two runs on one local Docker VM cover a several-minute overload and recovery window. They do not replace application-specific soak tests or a production capacity plan.
|
|
99
|
+
- The volume is disk-backed, but host/VM caches and virtualized flush behavior remain. This is not a cold-cache or physical disk I/O benchmark.
|
|
100
|
+
- Intentional SQL delay and retries make the backlog observable. The recorded throughput is specific to that handler and is not the package's maximum throughput.
|
|
101
|
+
- The ingress target is an offered schedule with bounded concurrency; actual acceptance throughput and missed targets are reported separately.
|
|
102
|
+
- Tenant results describe this 95/5 mix and the current shared scheduling behavior. No fairness SLA is claimed.
|
|
103
|
+
- This workload excludes broker traffic, RLS hooks, outbox writes, network faults, and failover. Separate operational scenarios verify those behaviors.
|
|
104
|
+
- Other repository test containers were stopped before the measured runs. Unrelated host activity was not controlled.
|