socketduct 0.1.25 → 0.1.26
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/AGENTS.md +2 -2
- package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
- package/package.json +2 -2
- package/scripts/test.js +9 -14
- package/src/logging/compressed-log-writer.js +1 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- package/test/e2e/gateway-process.test.js +93 -92
- package/test/e2e/logger-shutdown-process.test.js +40 -37
- package/test/e2e/reverse-client-recovery-process.test.js +107 -103
- package/test/e2e/reverse-gateway-process.test.js +328 -318
- package/test/integration/initial-target-recovery.test.js +333 -334
- package/test/integration/logging-cli.test.js +121 -119
- package/test/integration/logging-compressed-recovery.test.js +96 -95
- package/test/integration/multiplexed-compatibility.test.js +178 -177
- package/test/integration/multiplexed-corrections.test.js +360 -359
- package/test/integration/multiplexed-failures.test.js +253 -251
- package/test/integration/multiplexed-transport-set.test.js +334 -322
- package/test/integration/multiplexed-transport.test.js +135 -134
- package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
- package/test/integration/reverse-gateway.test.js +2309 -2218
- package/test/integration/reverse-resume-continuity.test.js +219 -218
- package/test/integration/reverse-route-class.test.js +358 -341
- package/test/integration/socketduct-http-agent.test.js +314 -312
- package/test/integration/socketduct-https-agent.test.js +516 -485
- package/test/integration/stream-compression.test.js +862 -824
- package/test/integration/tcp-forwarding.test.js +2550 -2447
- package/test/integration/tls-forwarding.test.js +593 -592
- package/test/unit/auth/auth.test.js +43 -44
- package/test/unit/auth/static-token-store.test.js +9 -8
- package/test/unit/client/connect.test.js +41 -40
- package/test/unit/client/deferred-virtual-socket.test.js +95 -94
- package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
- package/test/unit/client/multiplexed-transport-set.test.js +830 -748
- package/test/unit/client/socketduct-socket.test.js +768 -771
- package/test/unit/client/virtual-socket.test.js +198 -168
- package/test/unit/config/docker-gateway-files.test.js +224 -224
- package/test/unit/config/environment.test.js +472 -492
- package/test/unit/index.test.js +53 -52
- package/test/unit/logging/compressed-log-worker.test.js +23 -22
- package/test/unit/logging/compressed-log-writer.test.js +155 -154
- package/test/unit/logging/log-retention.test.js +52 -47
- package/test/unit/logging/log-segment-index.test.js +84 -81
- package/test/unit/logging/zstd-frame-codec.test.js +28 -19
- package/test/unit/protocol/compression.test.js +254 -298
- package/test/unit/protocol/frame.test.js +280 -276
- package/test/unit/protocol/v3-compression.test.js +21 -23
- package/test/unit/protocol/v3-frame.test.js +42 -41
- package/test/unit/release-configuration.test.js +15 -12
- package/test/unit/reverse/disconnect-attribution.test.js +65 -64
- package/test/unit/reverse-terminal-outcomes.test.js +53 -50
- package/test/unit/server/control-handler.test.js +232 -231
- package/test/unit/server/primary.test.js +51 -49
- package/test/unit/server/relay-runtime.test.js +280 -294
- package/test/unit/sessions/session.test.js +48 -62
- package/test/unit/spool/spool-file.test.js +401 -385
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/* eslint-disable jsdoc/require-returns */
|
|
2
|
-
import
|
|
2
|
+
import {describe, expect, it, withTestCleanup} from "@velocious/testing"
|
|
3
3
|
import {fork} from "node:child_process"
|
|
4
4
|
import {once} from "node:events"
|
|
5
5
|
import {mkdtemp, readFile, rm} from "node:fs/promises"
|
|
6
6
|
import {createServer} from "node:net"
|
|
7
7
|
import {tmpdir} from "node:os"
|
|
8
8
|
import {join} from "node:path"
|
|
9
|
-
import test from "node:test"
|
|
10
9
|
|
|
11
10
|
import {StaticTokenStore} from "../../src/auth/static-token-store.js"
|
|
12
11
|
import {OutboundReverseGateway, startReverseGatewayHub} from "../../src/reverse/index.js"
|
|
@@ -21,7 +20,9 @@ async function listen(server) {
|
|
|
21
20
|
server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(undefined) })
|
|
22
21
|
})
|
|
23
22
|
const address = server.address()
|
|
24
|
-
|
|
23
|
+
if (address === null || typeof address === "string") {
|
|
24
|
+
throw new Error("Server did not bind to a TCP port")
|
|
25
|
+
}
|
|
25
26
|
return address.port
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -63,111 +64,114 @@ async function waitForMessage(processState, type) {
|
|
|
63
64
|
return message
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
67
|
+
describe("reverse client process recovery", () => {
|
|
68
|
+
it("SIGKILLed reverse client process recovers its durable stream while hub gateway and target survive", async () => withTestCleanup(async (after) => {
|
|
69
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-client-process-recovery-"))
|
|
70
|
+
after(() => rm(root, {recursive: true, force: true}))
|
|
71
|
+
const certificates = generateTestCertificates(root)
|
|
72
|
+
const spoolDirectory = join(root, "client-spool")
|
|
73
|
+
let targetAccepts = 0
|
|
74
|
+
/** @type {import('node:net').Socket | undefined} */
|
|
75
|
+
let targetSocket
|
|
76
|
+
const targetSockets = new Set()
|
|
77
|
+
/** @type {Buffer[]} */
|
|
78
|
+
const targetReceived = []
|
|
79
|
+
const target = createServer((socket) => {
|
|
80
|
+
targetAccepts += 1
|
|
81
|
+
targetSocket = socket
|
|
82
|
+
targetSockets.add(socket)
|
|
83
|
+
socket.once("close", () => targetSockets.delete(socket))
|
|
84
|
+
socket.on("data", (chunk) => targetReceived.push(Buffer.from(chunk)))
|
|
85
|
+
})
|
|
86
|
+
const targetPort = await listen(target)
|
|
87
|
+
after(() => { for (const socket of targetSockets) socket.destroy(); return new Promise((resolve) => target.close(() => resolve(undefined))) })
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
const hub = await startReverseGatewayHub({
|
|
90
|
+
tokens: new StaticTokenStore([{username: "home", token: "gateway-token"}, {username: "app", token: "client-process-token"}]),
|
|
91
|
+
tls: {certFile: certificates.serverCertFile, keyFile: certificates.serverKeyFile},
|
|
92
|
+
authorizeGateway: () => true,
|
|
93
|
+
authorizeClientGateway: () => true,
|
|
94
|
+
authorizeOpen: () => true,
|
|
95
|
+
resumeWindowMs: 4_000
|
|
96
|
+
})
|
|
97
|
+
after(() => hub.close())
|
|
98
|
+
const gateway = new OutboundReverseGateway({
|
|
99
|
+
gatewayId: "o2",
|
|
100
|
+
connectionId: "process_recovery_gateway",
|
|
101
|
+
hub: {host: "127.0.0.1", port: hub.port, token: "gateway-token"},
|
|
102
|
+
tls: {caFile: certificates.caCertFile, servername: "localhost"},
|
|
103
|
+
spoolDirectory: join(root, "gateway-spool"),
|
|
104
|
+
authorizeTarget: () => true,
|
|
105
|
+
reconnectDelayMs: 10,
|
|
106
|
+
maxReconnectDelayMs: 20
|
|
107
|
+
})
|
|
108
|
+
after(() => gateway.close())
|
|
109
|
+
await gateway.start()
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
111
|
+
const configuration = {
|
|
112
|
+
gatewayId: "o2",
|
|
113
|
+
connectionId: "process_recovery_client",
|
|
114
|
+
hubPort: hub.port,
|
|
115
|
+
token: "client-process-token",
|
|
116
|
+
caFile: certificates.caCertFile,
|
|
117
|
+
spoolDirectory,
|
|
118
|
+
identifier: "process_recovery_stream",
|
|
119
|
+
sessionName: "durable-process-stream",
|
|
120
|
+
targetPort,
|
|
121
|
+
initialGeneration: 100
|
|
122
|
+
}
|
|
123
|
+
const original = startClient({...configuration, phase: "original"})
|
|
124
|
+
after(() => { if (original.child.exitCode === null) original.child.kill("SIGKILL") })
|
|
125
|
+
const connected = await waitForMessage(original, "connected")
|
|
126
|
+
original.child.send({type: "write", payload: Buffer.from("before-kill").toString("base64")})
|
|
127
|
+
await waitForMessage(original, "written")
|
|
128
|
+
await waitUntil(() => Buffer.concat(targetReceived).toString() === "before-kill", "original target request")
|
|
129
|
+
expect(targetAccepts).toBe(1)
|
|
130
|
+
if (targetSocket === undefined) throw new Error("Expected a live target socket")
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
original.child.send({type: "hold-next-data"})
|
|
133
|
+
await waitForMessage(original, "data-hold-armed")
|
|
134
|
+
original.child.send({type: "write", payload: Buffer.from("during-kill").toString("base64")})
|
|
135
|
+
await waitForMessage(original, "data-held")
|
|
136
|
+
await waitUntil(() => original.messages.filter((message) => message.type === "written").length === 2, "durably spooled held request")
|
|
137
|
+
expect(Buffer.concat(targetReceived).toString()).toBe("before-kill")
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
const replayedTargetBytes = Buffer.from("accepted-but-not-durably-acked")
|
|
140
|
+
original.child.send({type: "observe-buffer", bytes: replayedTargetBytes.length})
|
|
141
|
+
targetSocket.write(replayedTargetBytes)
|
|
142
|
+
await waitForMessage(original, "buffered")
|
|
143
|
+
original.child.send({type: "report-acks"})
|
|
144
|
+
const acknowledgements = await waitForMessage(original, "acks")
|
|
145
|
+
// Unread process-local bytes must not be durably acknowledged.
|
|
146
|
+
expect(acknowledgements.ackFrames).toBe(0)
|
|
147
|
+
await waitUntil(() => gateway.status().spoolBytes === replayedTargetBytes.length, "gateway retains unacknowledged target bytes")
|
|
148
|
+
const originalExit = once(original.child, "exit")
|
|
149
|
+
original.child.kill("SIGKILL")
|
|
150
|
+
await originalExit
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
const replacement = startClient({...configuration, phase: "replacement"})
|
|
153
|
+
after(() => { if (replacement.child.exitCode === null) replacement.child.kill("SIGKILL") })
|
|
154
|
+
const recovered = await waitForMessage(replacement, "recovered")
|
|
155
|
+
const replay = await waitForMessage(replacement, "data")
|
|
156
|
+
expect(Buffer.from(/** @type {string} */ (replay.payload), "base64").toString()).toBe(replayedTargetBytes.toString())
|
|
157
|
+
expect(recovered.openFrames).toBe(0)
|
|
158
|
+
expect(/** @type {number} */ (recovered.generation) > /** @type {number} */ (connected.generation)).toBeTruthy()
|
|
159
|
+
expect(targetAccepts).toBe(1)
|
|
160
|
+
replacement.child.send({type: "acknowledge", bytes: replayedTargetBytes.length})
|
|
161
|
+
const acknowledged = await waitForMessage(replacement, "acknowledged")
|
|
162
|
+
expect(acknowledged.sequence).toBe(replayedTargetBytes.length)
|
|
163
|
+
await waitUntil(() => Buffer.concat(targetReceived).toString() === "before-killduring-kill", "replay strictly above gateway high-water")
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
replacement.child.send({type: "write", payload: Buffer.from("after-kill").toString("base64")})
|
|
166
|
+
await waitForMessage(replacement, "written")
|
|
167
|
+
await waitUntil(() => Buffer.concat(targetReceived).toString() === "before-killduring-killafter-kill", "recovered target request")
|
|
168
|
+
expect(targetAccepts).toBe(1)
|
|
169
|
+
const persisted = await readFile(join(spoolDirectory, configuration.identifier, "recovery.json"), "utf8")
|
|
170
|
+
expect(persisted).not.toMatch(/client-process-token|gateway-token/)
|
|
168
171
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
const replacementExit = once(replacement.child, "exit")
|
|
173
|
+
replacement.child.send({type: "close"})
|
|
174
|
+
await replacementExit
|
|
175
|
+
if (replacement.child.exitCode !== 0) throw new Error(`Expected exit code 0: ${replacement.stderr()}`)
|
|
176
|
+
}))
|
|
173
177
|
})
|