socketduct 0.1.25 → 0.1.27
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/changelog.d/20261025070000-reclaim-unreadable-retention-lock.md +14 -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/log-retention.js +36 -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 +71 -48
- 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,11 +1,10 @@
|
|
|
1
1
|
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param, jsdoc/require-returns */
|
|
2
|
-
import
|
|
2
|
+
import {describe, expect, it, withTestCleanup} from "@velocious/testing"
|
|
3
3
|
import {EventEmitter} from "node:events"
|
|
4
4
|
import {mkdtemp, rm} from "node:fs/promises"
|
|
5
5
|
import {tmpdir} from "node:os"
|
|
6
6
|
import {join} from "node:path"
|
|
7
7
|
import {Duplex} from "node:stream"
|
|
8
|
-
import test from "node:test"
|
|
9
8
|
|
|
10
9
|
import {MultiplexedSocketductTransportSet} from "../../../src/client/multiplexed-transport-set.js"
|
|
11
10
|
import {decodeV3Control, encodeV3Control, encodeV3Frame, V3FrameDecoder, V3FrameType} from "../../../src/protocol/v3-frame.js"
|
|
@@ -102,7 +101,7 @@ function deferred() {
|
|
|
102
101
|
/** @template T @param {T[]} values - Values. @param {number} index - Index. @returns {T} */
|
|
103
102
|
function at(values, index) {
|
|
104
103
|
const value = values[index]
|
|
105
|
-
|
|
104
|
+
if (value === undefined) throw new Error(`expected a value at index ${index}`)
|
|
106
105
|
return value
|
|
107
106
|
}
|
|
108
107
|
|
|
@@ -145,165 +144,167 @@ async function settle() {
|
|
|
145
144
|
await new Promise((resolve) => setImmediate(resolve))
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
describe("multiplexed spool budget", () => {
|
|
148
|
+
it("destroy keeps shared spool capacity reserved until remove succeeds", async () => withTestCleanup(async (after) => {
|
|
149
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-destroy-spool-budget-"))
|
|
150
|
+
const originalRemove = SessionSpool.prototype.remove
|
|
151
|
+
const removeGate = deferred()
|
|
152
|
+
const removeCalled = deferred()
|
|
153
|
+
const removeFinished = deferred()
|
|
154
|
+
SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
|
|
155
|
+
if (identifier === "destroy_stream_01") {
|
|
156
|
+
removeCalled.resolve()
|
|
157
|
+
await removeGate.promise
|
|
158
|
+
}
|
|
159
|
+
await originalRemove.call(this, identifier)
|
|
160
|
+
if (identifier === "destroy_stream_01") removeFinished.resolve()
|
|
158
161
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await set.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await settle()
|
|
177
|
-
assert.equal(set.status().budgets.dataBytes, 0)
|
|
178
|
-
assert.equal(set.status().budgets.spoolBytes, 6)
|
|
162
|
+
const {set, controllers} = createFixture(root)
|
|
163
|
+
after(async () => {
|
|
164
|
+
removeGate.resolve()
|
|
165
|
+
SessionSpool.prototype.remove = originalRemove
|
|
166
|
+
await set.close().catch(() => {})
|
|
167
|
+
await rm(root, {recursive: true, force: true})
|
|
168
|
+
})
|
|
169
|
+
await set.start()
|
|
170
|
+
const first = set.createConnection({host: "target.internal", port: 443, identifier: "destroy_stream_01"})
|
|
171
|
+
await onceEvent(first, "connect")
|
|
172
|
+
expect(await writeResult(first, 6)).toBe(null)
|
|
173
|
+
await at(controllers, 0).nextFrame()
|
|
174
|
+
first.destroy()
|
|
175
|
+
await removeCalled.promise
|
|
176
|
+
await settle()
|
|
177
|
+
expect(set.status().budgets.dataBytes).toBe(0)
|
|
178
|
+
expect(set.status().budgets.spoolBytes).toBe(6)
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
})
|
|
180
|
+
const sibling = set.createConnection({host: "target.internal", port: 443, identifier: "destroy_stream_02"})
|
|
181
|
+
await onceEvent(sibling, "connect")
|
|
182
|
+
const blocked = await writeResult(sibling, 3)
|
|
183
|
+
let closeSettled = false
|
|
184
|
+
const closing = set.close().then(() => { closeSettled = true })
|
|
185
|
+
await settle()
|
|
186
|
+
expect(closeSettled).toBe(false)
|
|
187
|
+
removeGate.resolve()
|
|
188
|
+
await removeFinished.promise
|
|
189
|
+
await closing
|
|
190
|
+
expect(/** @type {{code?: string}} */ (blocked).code).toBe("SOCKETDUCT_SET_SPOOL_LIMIT")
|
|
191
|
+
expect(set.status().budgets.spoolBytes).toBe(0)
|
|
192
|
+
}))
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
194
|
+
it("failed destroy removal retains spool accounting and fails the member visibly", async () => withTestCleanup(async (after) => {
|
|
195
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-failed-remove-budget-"))
|
|
196
|
+
const originalRemove = SessionSpool.prototype.remove
|
|
197
|
+
const removeCalled = deferred()
|
|
198
|
+
SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
|
|
199
|
+
if (identifier === "failed_remove_stream_01") {
|
|
200
|
+
removeCalled.resolve()
|
|
201
|
+
throw new Error("injected remove failure")
|
|
202
|
+
}
|
|
203
|
+
await originalRemove.call(this, identifier)
|
|
202
204
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
await
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
assert.equal(at(set.status().members, 0).state, "closed")
|
|
223
|
-
})
|
|
205
|
+
const {set, controllers} = createFixture(root)
|
|
206
|
+
after(async () => {
|
|
207
|
+
SessionSpool.prototype.remove = originalRemove
|
|
208
|
+
await set.close().catch(() => {})
|
|
209
|
+
await rm(root, {recursive: true, force: true})
|
|
210
|
+
})
|
|
211
|
+
await set.start()
|
|
212
|
+
const socket = set.createConnection({host: "target.internal", port: 443, identifier: "failed_remove_stream_01"})
|
|
213
|
+
await onceEvent(socket, "connect")
|
|
214
|
+
expect(await writeResult(socket, 6)).toBe(null)
|
|
215
|
+
await at(controllers, 0).nextFrame()
|
|
216
|
+
socket.destroy()
|
|
217
|
+
await removeCalled.promise
|
|
218
|
+
await settle()
|
|
219
|
+
expect(set.status().budgets.dataBytes).toBe(0)
|
|
220
|
+
expect(set.status().budgets.spoolBytes).toBe(6)
|
|
221
|
+
expect(at(set.status().members, 0).spoolCleanupFailures).toBe(1)
|
|
222
|
+
expect(at(set.status().members, 0).state).toBe("closed")
|
|
223
|
+
}))
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
225
|
+
it("ACK keeps shared spool capacity reserved until truncate succeeds", async () => withTestCleanup(async (after) => {
|
|
226
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-ack-spool-budget-"))
|
|
227
|
+
const originalTruncate = SessionSpool.prototype.truncateAcked
|
|
228
|
+
const truncateGate = deferred()
|
|
229
|
+
const truncateCalled = deferred()
|
|
230
|
+
const truncateFinished = deferred()
|
|
231
|
+
SessionSpool.prototype.truncateAcked = async function (/** @type {string} */ identifier, /** @type {number} */ sequence) {
|
|
232
|
+
if (identifier === "ack_stream_01") {
|
|
233
|
+
truncateCalled.resolve()
|
|
234
|
+
await truncateGate.promise
|
|
235
|
+
}
|
|
236
|
+
await originalTruncate.call(this, identifier, sequence)
|
|
237
|
+
if (identifier === "ack_stream_01") truncateFinished.resolve()
|
|
235
238
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
await set.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
await settle()
|
|
254
|
-
assert.equal(set.status().budgets.dataBytes, 0)
|
|
255
|
-
assert.equal(set.status().budgets.spoolBytes, 6)
|
|
239
|
+
const {set, controllers} = createFixture(root)
|
|
240
|
+
after(async () => {
|
|
241
|
+
truncateGate.resolve()
|
|
242
|
+
SessionSpool.prototype.truncateAcked = originalTruncate
|
|
243
|
+
await set.close().catch(() => {})
|
|
244
|
+
await rm(root, {recursive: true, force: true})
|
|
245
|
+
})
|
|
246
|
+
await set.start()
|
|
247
|
+
const first = set.createConnection({host: "target.internal", port: 443, identifier: "ack_stream_01"})
|
|
248
|
+
await onceEvent(first, "connect")
|
|
249
|
+
expect(await writeResult(first, 6)).toBe(null)
|
|
250
|
+
const sent = await at(controllers, 0).nextFrame()
|
|
251
|
+
sent.socket.ack(sent.frame)
|
|
252
|
+
await truncateCalled.promise
|
|
253
|
+
await settle()
|
|
254
|
+
expect(set.status().budgets.dataBytes).toBe(0)
|
|
255
|
+
expect(set.status().budgets.spoolBytes).toBe(6)
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
})
|
|
257
|
+
const sibling = set.createConnection({host: "target.internal", port: 443, identifier: "ack_stream_02"})
|
|
258
|
+
await onceEvent(sibling, "connect")
|
|
259
|
+
const blocked = await writeResult(sibling, 3)
|
|
260
|
+
truncateGate.resolve()
|
|
261
|
+
await truncateFinished.promise
|
|
262
|
+
await settle()
|
|
263
|
+
expect(/** @type {{code?: string}} */ (blocked).code).toBe("SOCKETDUCT_SET_SPOOL_LIMIT")
|
|
264
|
+
expect(set.status().budgets.spoolBytes).toBe(0)
|
|
265
|
+
}))
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
267
|
+
it("failed ACK truncate and removal retain spool accounting and surface cleanup failure", async () => withTestCleanup(async (after) => {
|
|
268
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-failed-truncate-budget-"))
|
|
269
|
+
const originalTruncate = SessionSpool.prototype.truncateAcked
|
|
270
|
+
const originalRemove = SessionSpool.prototype.remove
|
|
271
|
+
const truncateCalled = deferred()
|
|
272
|
+
const removeCalled = deferred()
|
|
273
|
+
SessionSpool.prototype.truncateAcked = async function (/** @type {string} */ identifier, /** @type {number} */ sequence) {
|
|
274
|
+
if (identifier === "failed_ack_stream_01") {
|
|
275
|
+
truncateCalled.resolve()
|
|
276
|
+
throw new Error("injected truncate failure")
|
|
277
|
+
}
|
|
278
|
+
await originalTruncate.call(this, identifier, sequence)
|
|
277
279
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
|
|
281
|
+
if (identifier === "failed_ack_stream_01") {
|
|
282
|
+
removeCalled.resolve()
|
|
283
|
+
throw new Error("injected remove failure")
|
|
284
|
+
}
|
|
285
|
+
await originalRemove.call(this, identifier)
|
|
284
286
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
await
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
assert.equal(at(set.status().members, 0).state, "closed")
|
|
287
|
+
const {set, controllers} = createFixture(root)
|
|
288
|
+
after(async () => {
|
|
289
|
+
SessionSpool.prototype.truncateAcked = originalTruncate
|
|
290
|
+
SessionSpool.prototype.remove = originalRemove
|
|
291
|
+
await set.close().catch(() => {})
|
|
292
|
+
await rm(root, {recursive: true, force: true})
|
|
293
|
+
})
|
|
294
|
+
await set.start()
|
|
295
|
+
const socket = set.createConnection({host: "target.internal", port: 443, identifier: "failed_ack_stream_01"})
|
|
296
|
+
await onceEvent(socket, "connect")
|
|
297
|
+
expect(await writeResult(socket, 6)).toBe(null)
|
|
298
|
+
const sent = await at(controllers, 0).nextFrame()
|
|
299
|
+
const streamError = onceEvent(socket, "error").catch((error) => error)
|
|
300
|
+
sent.socket.ack(sent.frame)
|
|
301
|
+
await truncateCalled.promise
|
|
302
|
+
await removeCalled.promise
|
|
303
|
+
await streamError
|
|
304
|
+
await settle()
|
|
305
|
+
expect(set.status().budgets.dataBytes).toBe(0)
|
|
306
|
+
expect(set.status().budgets.spoolBytes).toBe(6)
|
|
307
|
+
expect(at(set.status().members, 0).spoolCleanupFailures).toBe(1)
|
|
308
|
+
expect(at(set.status().members, 0).state).toBe("closed")
|
|
309
|
+
}))
|
|
309
310
|
})
|