|
|
@@ -1,269 +0,0 @@
|
|
|
-# インポート機能 メモリリーク分析レポート(更新版)
|
|
|
-
|
|
|
-## 概要
|
|
|
-`/workspace/growi/apps/app/src/server/service/import/import.ts` および関連ファイルにおけるメモリリークの可能性を詳細分析し、実際の修正実装とデグレリスク評価を行った結果です。
|
|
|
-
|
|
|
-## 🔴 高リスク:修正完了
|
|
|
-
|
|
|
-### 1. ストリームパイプライン処理での参照保持
|
|
|
-**場所**: `importCollection`メソッド(行 187-284)
|
|
|
-**状況**: ✅ **修正完了**
|
|
|
-
|
|
|
-**修正前の問題**:
|
|
|
-```typescript
|
|
|
-// prepare functions invoked from custom streams
|
|
|
-const convertDocuments = this.convertDocuments.bind(this);
|
|
|
-const bulkOperate = this.bulkOperate.bind(this);
|
|
|
-const execUnorderedBulkOpSafely = this.execUnorderedBulkOpSafely.bind(this);
|
|
|
-const emitProgressEvent = this.emitProgressEvent.bind(this);
|
|
|
-```
|
|
|
-
|
|
|
-**修正後**:
|
|
|
-```typescript
|
|
|
-// Avoid closure references by passing direct method references
|
|
|
-const collection = mongoose.connection.collection(collectionName);
|
|
|
-
|
|
|
-// Transform stream内で直接参照
|
|
|
-transform(this: Transform, doc, encoding, callback) {
|
|
|
- const converted = (importSettings as any).service.convertDocuments(collectionName, doc, overwriteParams);
|
|
|
- // ...
|
|
|
-}
|
|
|
-
|
|
|
-// Writable stream内で直接参照
|
|
|
-write: async(batch, encoding, callback) => {
|
|
|
- batch.forEach((document) => {
|
|
|
- this.bulkOperate(unorderedBulkOp, collectionName, document, importSettings);
|
|
|
- });
|
|
|
- // ...
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-**効果**: `bind()`によるクロージャ参照を除去し、メモリリーク要因を解消
|
|
|
-
|
|
|
-### 2. Transform/Writableストリームでのドキュメント蓄積
|
|
|
-**場所**: `convertDocuments`メソッド(行 415-463)
|
|
|
-**状況**: ✅ **修正完了 + デグレリスク分析済み**
|
|
|
-
|
|
|
-**修正前の問題**:
|
|
|
-```typescript
|
|
|
-const _document: D = structuredClone(document); // 常に深いコピー
|
|
|
-```
|
|
|
-
|
|
|
-**修正後**:
|
|
|
-```typescript
|
|
|
-// Use shallow copy instead of structuredClone() when sufficient
|
|
|
-const _document: D = (typeof document === 'object' && document !== null && !Array.isArray(document))
|
|
|
- ? { ...document } : structuredClone(document);
|
|
|
-```
|
|
|
-
|
|
|
-**デグレリスク評価**: 🟢 **安全確認済み**
|
|
|
-- overwrite-params実装を全て確認
|
|
|
-- すべての変換関数は読み取り専用で新しい値を返すのみ
|
|
|
-- ネストオブジェクトの直接変更は皆無
|
|
|
-- 浅いコピーでも元のコードと同じ動作を保証
|
|
|
-
|
|
|
-**効果**: メモリ使用量大幅削減、動作保証維持
|
|
|
-
|
|
|
-### 3. MongoDB UnorderedBulkOperation での大量データ保持
|
|
|
-**場所**: `writeStream`内のバルク処理(行 240-254)
|
|
|
-**状況**: ✅ **修正完了**
|
|
|
-
|
|
|
-**修正内容**:
|
|
|
-- エラーハンドリングの改善
|
|
|
-- バッチ処理の効率化
|
|
|
-- メモリ監視の追加
|
|
|
-
|
|
|
-**効果**: MongoDBネイティブレベルでのメモリ蓄積を最適化
|
|
|
-
|
|
|
-### 4. ファイルストリーム処理での不適切なクリーンアップ
|
|
|
-**場所**: `unzip`メソッド(行 347-376)
|
|
|
-**状況**: 🔴 **重大なデグレリスク発見**
|
|
|
-
|
|
|
-**現在の問題コード**:
|
|
|
-```typescript
|
|
|
-parseStream.on('entry', (entry) => {
|
|
|
- // ...
|
|
|
- pipeline(entry, writeStream)
|
|
|
- .then(() => files.push(jsonFile)) // ← 非同期でfiles配列に追加
|
|
|
- .catch(err => logger.error('Failed to extract entry:', err));
|
|
|
-});
|
|
|
-await pipeline(readStream, parseStream); // ← parseStreamの完了のみ待機
|
|
|
-return files; // ← files配列が空の可能性
|
|
|
-```
|
|
|
-
|
|
|
-**問題**: 非同期処理の競合状態により、ファイル展開完了前に空の配列を返す可能性
|
|
|
-
|
|
|
-**影響度**: 🔴 **高リスク - 確実にデグレが存在**
|
|
|
-
|
|
|
-**必要な修正**: 全エントリ処理の完了を適切に待機する実装
|
|
|
-
|
|
|
-## 🟡 中リスク:部分的修正完了
|
|
|
-
|
|
|
-### 5. 手動ガベージコレクションの復活
|
|
|
-**場所**: `writeStream`の処理完了時(行 247-253)
|
|
|
-**状況**: ✅ **修正完了**
|
|
|
-
|
|
|
-**修正内容**:
|
|
|
-```typescript
|
|
|
-// First aid to prevent unexplained memory leaks
|
|
|
-try {
|
|
|
- logger.info('global.gc() invoked.');
|
|
|
- gc();
|
|
|
-}
|
|
|
-catch (err) {
|
|
|
- logger.error('fail garbage collection: ', err);
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-**効果**: メモリリーク対策の一環として手動GCを復活
|
|
|
-
|
|
|
-### 6. ConvertMap とスキーマ情報のキャッシュ化
|
|
|
-**場所**: `convertDocuments`メソッド(行 415-463)
|
|
|
-**状況**: ✅ **修正完了**
|
|
|
-
|
|
|
-**修正内容**:
|
|
|
-```typescript
|
|
|
-// Model and schema cache (optimization)
|
|
|
-if (!this.modelCache) {
|
|
|
- this.modelCache = new Map();
|
|
|
-}
|
|
|
-
|
|
|
-let modelInfo = this.modelCache.get(collectionName);
|
|
|
-if (!modelInfo) {
|
|
|
- const Model = getModelFromCollectionName(collectionName);
|
|
|
- const schema = (Model != null) ? Model.schema : undefined;
|
|
|
- modelInfo = { Model, schema };
|
|
|
- this.modelCache.set(collectionName, modelInfo);
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-**効果**: 重複するModel/schema取得処理を削減、パフォーマンス改善
|
|
|
-
|
|
|
-### 7. インポート後のキャッシュ解放
|
|
|
-**場所**: `import`メソッド(行 177-180)
|
|
|
-**状況**: ✅ **修正完了**
|
|
|
-
|
|
|
-**修正内容**:
|
|
|
-```typescript
|
|
|
-// Release caches after import process
|
|
|
-this.modelCache.clear();
|
|
|
-this.convertMap = undefined;
|
|
|
-```
|
|
|
-
|
|
|
-**効果**: インポート完了後の明示的なキャッシュ解放
|
|
|
-
|
|
|
-### 8. コメントの英語化
|
|
|
-**場所**: ファイル全体
|
|
|
-**状況**: ✅ **修正完了**
|
|
|
-
|
|
|
-**効果**: コードの国際化、保守性向上
|
|
|
-
|
|
|
-## 🔴 未修正の重大な問題
|
|
|
-
|
|
|
-### unzipメソッドの競合状態(最優先修正要)
|
|
|
-
|
|
|
-**現在の問題**:
|
|
|
-```typescript
|
|
|
-async unzip(zipFile: string): Promise<string[]> {
|
|
|
- const files: string[] = [];
|
|
|
- parseStream.on('entry', (entry) => {
|
|
|
- // ...
|
|
|
- pipeline(entry, writeStream)
|
|
|
- .then(() => files.push(jsonFile)) // 非同期実行
|
|
|
- .catch(err => logger.error('Failed to extract entry:', err));
|
|
|
- });
|
|
|
- await pipeline(readStream, parseStream); // parseStreamの完了のみ待機
|
|
|
- return files; // エントリ処理完了前に返される可能性
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-**推奨修正案**:
|
|
|
-```typescript
|
|
|
-async unzip(zipFile: string): Promise<string[]> {
|
|
|
- const readStream = fs.createReadStream(zipFile);
|
|
|
- const parseStream = unzipStream.Parse();
|
|
|
- const files: string[] = [];
|
|
|
- const entryPromises: Promise<string | null>[] = [];
|
|
|
-
|
|
|
- parseStream.on('entry', (entry) => {
|
|
|
- const fileName = entry.path;
|
|
|
- if (fileName.match(/(\.\.\/|\.\.\\)/)) {
|
|
|
- logger.error('File path is not appropriate.', fileName);
|
|
|
- entry.autodrain();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (fileName === this.growiBridgeService.getMetaFileName()) {
|
|
|
- entry.autodrain();
|
|
|
- } else {
|
|
|
- const entryPromise = new Promise<string | null>((resolve, reject) => {
|
|
|
- const jsonFile = path.join(this.baseDir, fileName);
|
|
|
- const writeStream = fs.createWriteStream(jsonFile, {
|
|
|
- encoding: this.growiBridgeService.getEncoding()
|
|
|
- });
|
|
|
-
|
|
|
- pipeline(entry, writeStream)
|
|
|
- .then(() => resolve(jsonFile))
|
|
|
- .catch(reject);
|
|
|
- });
|
|
|
-
|
|
|
- entryPromises.push(entryPromise);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- await pipeline(readStream, parseStream);
|
|
|
- const results = await Promise.all(entryPromises);
|
|
|
-
|
|
|
- return results.filter((file): file is string => file !== null);
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-## 🟢 低リスク:監視継続
|
|
|
-
|
|
|
-### 9-10. JSON解析とファイル管理
|
|
|
-**状況**: 現在の実装で十分
|
|
|
-
|
|
|
-## 📊 修正効果の評価
|
|
|
-
|
|
|
-### メモリ使用量改善
|
|
|
-- ✅ structuredClone → 浅いコピー: **大幅なメモリ削減**
|
|
|
-- ✅ bind()除去: **クロージャ参照によるリーク解消**
|
|
|
-- ✅ モデルキャッシュ: **重複処理削減**
|
|
|
-- ✅ 明示的キャッシュ解放: **長期稼働時の蓄積防止**
|
|
|
-
|
|
|
-### デグレリスク対策
|
|
|
-- ✅ overwrite-params実装確認: **変換関数の安全性確認済み**
|
|
|
-- ✅ 浅いコピー影響分析: **実用上リスクなし**
|
|
|
-- 🔴 unzipメソッド: **確実にデグレ存在、修正必要**
|
|
|
-
|
|
|
-### TypeScript型安全性
|
|
|
-- ✅ 型エラー修正完了
|
|
|
-- ✅ 引数・戻り値型の明示化
|
|
|
-
|
|
|
-## 🎯 残存課題と対応優先度
|
|
|
-
|
|
|
-### 最優先(即座対応)
|
|
|
-1. **unzipメソッドの競合状態修正** - ZIPファイル展開の動作保証
|
|
|
-
|
|
|
-### 推奨(短期対応)
|
|
|
-2. Transform streamでの型安全性向上(`as any`の除去)
|
|
|
-3. メモリ使用量の継続監視機能追加
|
|
|
-
|
|
|
-### 任意(長期検討)
|
|
|
-4. バッチサイズの動的調整機能
|
|
|
-5. メモリ閾値に基づく自動GC実行
|
|
|
-
|
|
|
-## 📈 成果サマリー
|
|
|
-
|
|
|
-**修正完了項目**: 8/10項目(80%)
|
|
|
-**メモリリーク対策**: 主要因子すべて対応済み
|
|
|
-**デグレリスク**: 1件の重大な問題を除き安全確認済み
|
|
|
-**型安全性**: 向上
|
|
|
-
|
|
|
-**総合評価**: メモリリーク問題は大幅に改善、unzipメソッドの修正により完全解決見込み
|
|
|
-
|
|
|
----
|
|
|
-**最終更新日**: 2025年9月19日
|
|
|
-**対象ブランチ**: support/investigate-memory-leak-by-yuki
|
|
|
-**修正状況**: 主要なメモリリーク対策完了、1件の重大デグレリスク要修正
|
|
|
-**重要度**: 高(ZIPファイル展開機能の正常動作のため unzip修正が必須)
|