From a3f4a7c00459002a64c72edc6dc48f6a6157955c Mon Sep 17 00:00:00 2001 From: flfeders Date: Mon, 28 Sep 2026 14:08:59 +0200 Subject: [PATCH] fix(storage): verify migration of exports up to one GiB --- docs/minio-umstellung-pilot.md | 13 ++++++------- scripts/storage/README.md | 4 ++-- scripts/storage/s3.mjs | 4 ++-- scripts/storage/tests/integration.mjs | 26 ++++++++++++++++++++++---- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/minio-umstellung-pilot.md b/docs/minio-umstellung-pilot.md index f0dea093..f715c32f 100644 --- a/docs/minio-umstellung-pilot.md +++ b/docs/minio-umstellung-pilot.md @@ -1,6 +1,6 @@ # MinIO → SeaweedFS: betreuter Pilot auf einer Installer-Instanz -Stand: 27. September 2026. Dieser Ablauf ergänzt das +Stand: 28. September 2026. Dieser Ablauf ergänzt das [Austauschkonzept](minio-austausch-konzept.md) um ausführbare Werkzeuge. Er übernimmt den Speicher einer vorhandenen Installation, beispielsweise 3.3.0, mit unveränderten Anwendungsimages und ohne Datenbankschemamigration. @@ -8,7 +8,7 @@ mit unveränderten Anwendungsimages und ohne Datenbankschemamigration. **Vor dem ersten Wartungsfenster nur `load-images` und `plan` ausführen.** Die Vorprüfung ermittelt die bislang unbekannten Größen und liefert eine kompakte Ausgabe zum gemeinsamen Prüfen. Sie stoppt keine Anwendungscontainer. -Ein Zugriff auf die konkrete Kundeninstanz wurde bei der Entwicklung nicht ausgeführt. +Die fachliche Abnahme erfolgt zusätzlich auf der konkreten Instanz. ## Was der Pilot leistet und wo seine Grenze liegt @@ -72,7 +72,7 @@ aus der S3-Liste verbergen, obwohl er per GET erreichbar bleibt. Bei gestopptem MinIO muss deshalb die Menge lokaler `xl.meta`-Objektpfade exakt mit dem S3-Inventar übereinstimmen. Andere Speicherformate oder Abweichungen blockieren die Umschaltung. -Getestete Pilotgrenzen: **200.000 Objekte, höchstens 256 MiB je Objekt**. +Getestete Pilotgrenzen: **200.000 Objekte, höchstens 1 GiB je Objekt**. Die Kapazitätsabschätzung berücksichtigt Quellsicherung, neues und wiederhergestelltes Ziel, Metadatenreserve pro Objekt, DB-Dump, Offline-Images, Spool und 5 GiB Reserve. Nach dem Schreibstopp wird das Inventar erneut ermittelt. Es gibt keine Annahme, @@ -211,14 +211,13 @@ nicht automatisch gelöscht. ## Nachgewiesene Tests und ausstehende Freigaben Der lokale S3-Test verwendet das offizielle MinIO-Release-Binary vom 7. September -2025 und SeaweedFS 4.47 mit festem Digest. 1.005 Objekte / 268.446.382 Bytes, -einschließlich 256-MiB-Grenzdatei, Pagination, Sonderzeichen, Metadaten, leeren +2025 und SeaweedFS 4.47 mit festem Digest. 1.005 Objekte / 1.073.752.750 Bytes, +einschließlich 1-GiB-Grenzdatei, Pagination, Sonderzeichen, Metadaten, leeren Dateien, Streaming, 24-Stunden-Signaturen, fremdem Bucket und ungültigen Zugängen werden geprüft. Beschädigte Zielobjekte werden erkannt und beim Fortsetzen erneut kopiert; der kalte Restore wird vollständig gelesen und verglichen. -Ein Lauf der Kopier-/Prüf-/Restore-Sequenz dauerte lokal ungefähr 65 Sekunden; Testhost: Linux, 16 logische CPUs, rund 29 GiB RAM für Docker, lokaler NVMe-Speicher. -Dies ist keine Laufzeitzusage für die Instanz. +Die Laufzeit hängt von Objektanzahl, Datenmenge und Host ab. Das Hilfs-Lockfile enthält den übernommenen vollständigen SDK-Abhängigkeitsbaum des Backends, nicht nur dieselbe oberste Versionsnummer. Beim Pilot wird außerdem diff --git a/scripts/storage/README.md b/scripts/storage/README.md index 3def70dc..3ffd2609 100644 --- a/scripts/storage/README.md +++ b/scripts/storage/README.md @@ -17,7 +17,7 @@ python3 -B scripts/storage/tests/host_integration.py ``` Der S3-Test verwendet ausschließlich neu angelegte Testcontainer mit künstlichen -Daten. Er prüft über 1.000 Objekte, Pagination, 256 MiB, leere Dateien, +Daten. Er prüft über 1.000 Objekte, Pagination, eine 1-GiB-Datei, leere Dateien, Sonderzeichen, Metadaten, SigV4, 24-Stunden-Links, Streaming, verbotene Zugriffe, beschädigte Zielobjekte, erneute Kopie und echten Restore in ein frisches Verzeichnis. Die gesicherten Testdaten bleiben unter `/tmp/fedeo-storage-*` @@ -27,7 +27,7 @@ Der Host-Test verwendet einen echten PostgreSQL-/MinIO-/SeaweedFS-Stack und künstliche Anwendungscontainer. Er ersetzt keine fachliche Abnahme von FEDEO (PDF, WebDAV, Exporte, Mandantenrechte) auf der konkreten Instanz. -Die erste Übernahme ist auf 200.000 Objekte und 256 MiB pro Objekt begrenzt. +Die erste Übernahme ist auf 200.000 Objekte und 1 GiB pro Objekt begrenzt. Das größte Objekt wird auf Platte gespult; RAM-Verbrauch wächst nicht mit dessen Größe. Multipart-Übernahmen und Sonderkonfigurationen werden abgelehnt. diff --git a/scripts/storage/s3.mjs b/scripts/storage/s3.mjs index 9da4c33c..3be02aa5 100644 --- a/scripts/storage/s3.mjs +++ b/scripts/storage/s3.mjs @@ -15,7 +15,7 @@ const S3 = requireSdk('@aws-sdk/client-s3'); const { getSignedUrl } = requireSdk('@aws-sdk/s3-request-presigner'); // The pilot deliberately has a tested bound; no untested multipart conversion. -export const MAX_OBJECT_BYTES = 256 * 1024 * 1024; +export const MAX_OBJECT_BYTES = 1024 * 1024 * 1024; export const MAX_OBJECTS = 200_000; export const client = config => new S3.S3Client({ ...config, forcePathStyle: true, maxAttempts: 3 }); export const call = (s3, operation, input) => s3.send(new S3[`${operation}Command`](input)); @@ -45,7 +45,7 @@ export function validateKeys(objects) { requireThat(!/[\x00-\x1f\x7f\\]/.test(key) && segments.every(s => s && s !== '.' && s !== '..'), `Nicht verlustfrei übertragbarer Schlüssel: ${JSON.stringify(key)}`); requireThat(Number.isSafeInteger(size) && size >= 0 && size <= MAX_OBJECT_BYTES, - `Objekt außerhalb der getesteten Größe (256 MiB): ${JSON.stringify(key)}`); + `Objekt außerhalb der getesteten Größe (${MAX_OBJECT_BYTES / 1024 / 1024} MiB): ${JSON.stringify(key)}`); for (let i = 1; i < segments.length; i++) { requireThat(!keys.has(segments.slice(0, i).join('/')), `Datei-/Verzeichniskonflikt: ${JSON.stringify(key)}`); } diff --git a/scripts/storage/tests/integration.mjs b/scripts/storage/tests/integration.mjs index 018f7d8c..f2fc1c71 100644 --- a/scripts/storage/tests/integration.mjs +++ b/scripts/storage/tests/integration.mjs @@ -1,6 +1,6 @@ // Explicit integration run; never discovers or connects to an existing FEDEO stack. import assert from 'node:assert/strict'; -import { execFileSync } from 'node:child_process'; +import { execFileSync, spawnSync } from 'node:child_process'; import { mkdtemp, mkdir, writeFile, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -28,6 +28,20 @@ async function ready(s3) { } throw new Error('S3 did not become ready'); } +async function restoredContentsReady(s3, bucket, report) { + // Like the host helper, wait for volume registration after a cold start. + // ListBuckets can already succeed while reading object data still returns 500. + const deadline = Date.now() + 180_000; + for (;;) { + try { return await verifyManifest(s3, bucket, report); } + catch (error) { + const transient = [500, 502, 503, 504].includes(error.$metadata?.httpStatusCode) || + ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(error.code); + if (!transient || Date.now() >= deadline) throw error; + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } +} async function weed(name, data) { await mkdir(join(data, 'master'), { recursive: true }); run(name, '-p', '127.0.0.1::9000', '-v', `${data}:/data`, '-v', `${root}/config:/etc/fedeo:ro`, @@ -73,11 +87,12 @@ try { Body: Buffer.from(`fixture-${i}`), ContentType: 'text/plain', Metadata: { custom: 'preserved' } }); await call(source, 'PutObject', { Bucket, Key: 'empty', Body: Buffer.alloc(0), ContentType: 'application/octet-stream' }); await call(source, 'PutObject', { Bucket, Key: 'large.zip', ContentLength: MAX_OBJECT_BYTES, - Body: Readable.from((function* () { for (let i = 0; i < 256; i++) yield Buffer.alloc(1024 * 1024, i); })()), + Body: Readable.from((function* () { for (let i = 0; i < MAX_OBJECT_BYTES / (1024 * 1024); i++) yield Buffer.alloc(1024 * 1024, i % 256); })()), ContentType: 'application/zip', ContentDisposition: 'attachment; filename="large.zip"', CacheControl: 'private', ContentLanguage: 'de', Expires: new Date('2030-01-01T00:00:00Z') }); const before = await inventory(source, Bucket); assert.equal(before.count, 1005); + assert.equal(before.objects.find(o => o.Key === 'large.zip').Size, MAX_OBJECT_BYTES); await call(source, 'PutObject', { Bucket, Key: 'unsupported-marker/', Body: '' }); await assert.rejects(inventory(source, Bucket), /Schlüssel/); await call(source, 'DeleteObject', { Bucket, Key: 'unsupported-marker/' }); @@ -95,7 +110,7 @@ try { const restoredAdmin = client({ ...restoredConfig, credentials: adminCredentials }); try { await ready(restoredAdmin); - await verifyManifest(restored, Bucket, report); + await restoredContentsReady(restored, Bucket, report); await contract(restored, restoredConfig, Bucket, { foreignBucket: 'foreign-private' }); } finally { restored.destroy(); restoredAdmin.destroy(); } console.log(JSON.stringify({ result: 'passed', objects: report.count, bytes: report.bytes, @@ -103,7 +118,10 @@ try { process.env.FEDEO_MINIO_TEST_IMAGE || 'fedeo-minio-fixture:2025-09-07', '--format', '{{.Id}}'), evidence: root }, null, 2)); } catch (error) { - for (const name of containers) { try { console.error(docker('logs', '--tail', '12', name)); } catch {} } + for (const name of containers) { + const logs = spawnSync('docker', ['logs', '--tail', '30', name], { encoding: 'utf8' }); + console.error(logs.stdout + logs.stderr); + } throw error; } finally { source?.destroy(); target?.destroy(); admin?.destroy();