diff --git a/package-lock.json b/package-lock.json index f622e3b..1ef382a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "sharp": "^0.35.3", "tsx": "4.23.12", "typescript": "6.0.3", + "undici": "^7.29.0", "wrangler": "4.125.0" } }, diff --git a/package.json b/package.json index 099f86a..dccabba 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "sharp": "^0.35.3", "tsx": "4.23.12", "typescript": "6.0.3", + "undici": "^7.29.0", "wrangler": "4.125.0" }, "overrides": { diff --git a/scripts/orbit-mcp-authorization-tests.ts b/scripts/orbit-mcp-authorization-tests.ts index 73dddff..4bbee05 100644 --- a/scripts/orbit-mcp-authorization-tests.ts +++ b/scripts/orbit-mcp-authorization-tests.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { after, before, describe, test } from 'node:test'; import { reserveWorkerPorts } from './orbit-test-ports'; +import { useFreshConnectionPerRequest } from './support/test-http'; import { BACKUP_SCHEMA_VERSION } from '../src/server/backup/dynamic-backup'; import { createOpaqueToken, @@ -12,6 +13,10 @@ import { verifyOpaqueToken, } from '../src/server/identity/tokens'; +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); + const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const CONFIG = 'wrangler.test.jsonc'; diff --git a/scripts/orbit-site-authorization-tests.ts b/scripts/orbit-site-authorization-tests.ts index 7aaecb6..fd59c07 100644 --- a/scripts/orbit-site-authorization-tests.ts +++ b/scripts/orbit-site-authorization-tests.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { after, before, describe, test } from 'node:test'; import { reserveWorkerPorts } from './orbit-test-ports'; +import { useFreshConnectionPerRequest } from './support/test-http'; import { normalizeSiteAuthorizationScopes, parseSiteAuthorizationScopes, @@ -30,6 +31,10 @@ import { } from '../src/server/identity/site-authorization-request'; import { siteConsentPage } from '../src/server/http/site-consent-page'; +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); + const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const CONFIG = 'wrangler.test.jsonc'; diff --git a/scripts/orbit-site-client-registration-tests.ts b/scripts/orbit-site-client-registration-tests.ts index 3c5c480..2c16353 100644 --- a/scripts/orbit-site-client-registration-tests.ts +++ b/scripts/orbit-site-client-registration-tests.ts @@ -21,8 +21,13 @@ import path from 'node:path'; import { after, before, describe, test } from 'node:test'; import { reserveWorkerPorts } from './orbit-test-ports'; +import { useFreshConnectionPerRequest } from './support/test-http'; import { createOpaqueToken, hmacDigest, randomBase64Url } from '../src/server/identity/tokens'; +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); + const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const CONFIG = 'wrangler.slice1-test.jsonc'; diff --git a/scripts/orbit-site-signin-e2e-tests.ts b/scripts/orbit-site-signin-e2e-tests.ts index 51c721c..64fe7a9 100644 --- a/scripts/orbit-site-signin-e2e-tests.ts +++ b/scripts/orbit-site-signin-e2e-tests.ts @@ -18,6 +18,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { after, before, describe, test } from 'node:test'; import { reserveWorkerPorts } from './orbit-test-ports'; +import { useFreshConnectionPerRequest } from './support/test-http'; import { createOpaqueToken, hmacDigest, @@ -25,6 +26,10 @@ import { sha256Base64Url, } from '../src/server/identity/tokens'; +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); + const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const CONFIG = 'wrangler.slice1-test.jsonc'; diff --git a/scripts/orbit-slice1-tests.ts b/scripts/orbit-slice1-tests.ts index 5450175..9cead3d 100644 --- a/scripts/orbit-slice1-tests.ts +++ b/scripts/orbit-slice1-tests.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { after, before, describe, test } from 'node:test'; import { reserveWorkerPorts } from './orbit-test-ports'; +import { useFreshConnectionPerRequest } from './support/test-http'; import { CSRF_COOKIE, CSRF_HEADER, @@ -21,6 +22,10 @@ import { } from '../src/server/identity/tokens'; import { LEGAL_LAST_UPDATED } from '../src/data/legal'; +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); + const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const CONFIG = 'wrangler.slice1-test.jsonc'; diff --git a/scripts/orbit-slice3-tests.ts b/scripts/orbit-slice3-tests.ts index 116e6fe..a0b4a4a 100644 --- a/scripts/orbit-slice3-tests.ts +++ b/scripts/orbit-slice3-tests.ts @@ -5,8 +5,13 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { after, before, describe, test } from 'node:test'; import { reserveWorkerPorts } from './orbit-test-ports'; +import { useFreshConnectionPerRequest } from './support/test-http'; import { loadManifest, verifyManifest } from './orbit-slice3-manifest'; +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); + const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const CONFIG = 'wrangler.slice1-test.jsonc'; diff --git a/scripts/orbit-slice4-tests.ts b/scripts/orbit-slice4-tests.ts index b734898..337a591 100644 --- a/scripts/orbit-slice4-tests.ts +++ b/scripts/orbit-slice4-tests.ts @@ -5,6 +5,7 @@ import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { after, before, describe, test } from 'node:test'; import { reserveWorkerPorts } from './orbit-test-ports'; +import { useFreshConnectionPerRequest } from './support/test-http'; import { createEntityId } from '../src/server/foundation/ids'; import { createOpaqueToken, hmacDigest, randomBase64Url, sha256Base64Url } from '../src/server/identity/tokens'; import { canonicalJson } from '../src/server/publication/content'; @@ -15,6 +16,10 @@ import { type DynamicBackup, } from '../src/server/backup/dynamic-backup'; +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); + const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const TSX = path.join(ROOT, 'node_modules', 'tsx', 'dist', 'cli.mjs'); diff --git a/scripts/orbit-slice5-tests.ts b/scripts/orbit-slice5-tests.ts index acd6bda..8c4e4b5 100644 --- a/scripts/orbit-slice5-tests.ts +++ b/scripts/orbit-slice5-tests.ts @@ -5,6 +5,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { after, before, describe, test } from 'node:test'; import { reserveWorkerPorts } from './orbit-test-ports'; +import { useFreshConnectionPerRequest } from './support/test-http'; import { createEntityId } from '../src/server/foundation/ids'; import { createOpaqueToken, hmacDigest, randomBase64Url, sha256Base64Url } from '../src/server/identity/tokens'; import { canonicalJson } from '../src/server/publication/content'; @@ -17,6 +18,10 @@ import { verifyDynamicBackup } from '../src/server/backup/dynamic-backup'; import { ImageTransformError, inspectImage, transformImage } from '../src/server/media/image-processor'; import sharp from 'sharp'; +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); + const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const TSX = path.join(ROOT, 'node_modules', 'tsx', 'dist', 'cli.mjs'); diff --git a/scripts/support/d1-test-worker-harness.ts b/scripts/support/d1-test-worker-harness.ts index d0a835b..2e6860b 100644 --- a/scripts/support/d1-test-worker-harness.ts +++ b/scripts/support/d1-test-worker-harness.ts @@ -11,6 +11,11 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { reserveWorkerPorts } from '../orbit-test-ports'; +import { useFreshConnectionPerRequest } from './test-http'; + +/* Havuzda bekleyen bir keep-alive soketi, bu dosyanın spawnSync + * bloklarından sağ çıkmıyor; gerekçesi support/test-http.ts içinde. */ +useFreshConnectionPerRequest(); const ROOT = process.cwd(); const WRANGLER = path.join(ROOT, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); diff --git a/scripts/support/test-http.ts b/scripts/support/test-http.ts new file mode 100644 index 0000000..eb77f17 --- /dev/null +++ b/scripts/support/test-http.ts @@ -0,0 +1,79 @@ +/** + * Yerel test worker'larına giden fetch'lerin bağlantı politikası. + * + * Belirti şuydu: CI'da ara sıra, bir testin ilk `fetch`'i hiçbir iddiaya + * varmadan 3-4 ms içinde düşüyordu. + * + * TypeError: fetch failed + * cause: SocketError: other side closed (UND_ERR_SOCKET) + * socket: { bytesWritten: 370, bytesRead: 1631 } + * + * `bytesRead` sıfır değil: soket daha önce bir yanıt taşımış, yani bu yeni + * kurulan bir bağlantı değil, undici'nin havuzundan gelen bir keep-alive + * bağlantısı. Sunucu onu kapatmış (FIN), istemci ölü sokete yazmış. + * + * Neden istemci FIN'i görmüyor: bu test dosyaları worker ayaktayken + * `spawnSync` ile ikinci wrangler süreçleri koşturuyor (migration, importer, + * d1 execute). `spawnSync` olay döngüsünü saniyelerce tamamen durduruyor. + * O sürede undici ne soketin FIN'ini işleyebiliyor ne de kendi boşta kalma + * zamanlayıcısını (varsayılan 4 sn) çalıştırıp soketi havuzdan atabiliyor. + * `spawnSync` döner dönmez gelen `fetch` isteği aynı senkron adımda ölü + * sokete yazılıyor. Sunucu tarafında da bir güvence yok: workerd yanıtta + * `Keep-Alive` başlığı göndermiyor, yani boşta kalan bir bağlantıyı ne + * zaman kapatacağını istemciye hiç söylemiyor. + * + * HTTP/1.1'de bunun sağlıklı bir çözümü yok: yazmadan önce bağlantının + * hâlâ açık olup olmadığını bilmenin yolu yoktur. Normal istemciler bunu + * "güvenli isteği bir kez tekrar dene" ile örter; testte hatayı örtmek + * istemiyoruz. Kalan tek sağlam seçenek bağlantıyı hiç yeniden + * kullanmamak: `pipelining: 0` undici'de keep-alive'ı kapatır, her istek + * kendi bağlantısını açar ve yarış ortadan kalkar. Yerelde birkaç düzine + * istek için maliyeti ölçülemeyecek kadar küçük. + */ +import { Agent, setGlobalDispatcher } from 'undici'; +import type { Dispatcher } from 'undici'; + +let installed = false; + +/* Node'un global `fetch`'i dispatcher'ı `Symbol.for('undici.globalDispatcher.1')` + * üzerinden okuyor; undici paketinin `setGlobalDispatcher`'ı aynı sembole + * yazdığı için yerleşik fetch de bu ayarı görüyor. */ +export function useFreshConnectionPerRequest(): void { + if (installed) return; + const agent = new Agent({ pipelining: 0 }); + setGlobalDispatcher(agent); + assertDispatcherInstalled(agent); + installed = true; +} + +/* Bekçi: ayarın gerçekten yerleşip yerleşmediğini kontrol eder. + * + * `setGlobalDispatcher` global bir sembole yazıyor ve Node'un yerleşik + * `fetch`'i onu okuyor. Bugün (Node 26 / undici 7) iki sembol de — + * `undici.globalDispatcher.1` ve `.2` — aynı nesneyi gösteriyor. Yarın + * paket ile Node'un sembolleri ayrışırsa çağrı sessizce hiçbir şey yapmaz: + * keep-alive geri gelir, flake geri gelir ve hiçbir test kırılmaz, çünkü + * bu yalnızca ara sıra düşen bir yarışı geri açar. + * + * Sessiz bozulmayı gürültülü hataya çeviriyoruz: yerleşmediyse testler + * daha ilk satırda dursun. */ +function assertDispatcherInstalled(expected: Dispatcher): void { + const seen = [ + Symbol.for('undici.globalDispatcher.1'), + Symbol.for('undici.globalDispatcher.2'), + ].filter((symbol) => symbol in globalThis); + + const missed = seen.filter( + (symbol) => (globalThis as Record)[symbol] !== expected, + ); + + if (seen.length === 0 || missed.length > 0) { + throw new Error( + 'Test fetch bağlantı politikası yerleşmedi: undici setGlobalDispatcher ' + + "Node'un yerleşik fetch'inin okuduğu sembole yazmıyor. Keep-alive " + + 'yeniden kullanımı geri döner ve UND_ERR_SOCKET flake\'i yeniden ' + + 'başlar. support/test-http.ts içindeki gerekçeye bak; undici ve Node ' + + 'sürümlerinin uyumunu kontrol et.', + ); + } +}