开玩笑的猫鼬导致内存泄漏

Posted

技术标签:

【中文标题】开玩笑的猫鼬导致内存泄漏【英文标题】:jest mongoose causing memory leak 【发布时间】:2020-12-31 00:18:13 【问题描述】:

2020 年 9 月 14 日更新

我有一个我编写的示例测试用例。当我运行这段代码时,测试用例通过了,但它抱怨说拆解没有正确发生并且有一个打开的连接。任何人都可以发现它是什么:

方法 1 - 内存泄漏

import  Connection, createConnection  from 'mongoose';
import __MONGO_URI__ from './__MONGO_URI__';

let conn: Connection | null = null;

const getConnection: (MONGO_DB_NAME: string) => Promise<Connection> = async MONGO_DB_NAME => 
  if (conn == null) 
    conn = await createConnection(__MONGO_URI__, 
      dbName: MONGO_DB_NAME,
      bufferCommands: false, // Disable mongoose buffering
      bufferMaxEntries: 0, // and MongoDB driver buffering
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true
    );
  
  return conn;
;

const MONGO_DB_NAME = 'mongo-test';
let db: Connection;

describe('mongo - connection test to ensure setup teardown', () => 
  beforeAll(async done => 
    db = await getConnection(MONGO_DB_NAME);
    done();
  );
  afterAll(async done => 
    if (conn) 
      await db.dropDatabase();
      await conn.close();
    
    done();
  );
  it('true = true', () => 
    expect(true).toBe(true);
  );
);

错误:

工作进程未能正常退出并被强制退出。这可能是由于不正确的拆卸导致测试泄漏造成的。尝试使用 --runInBand --detectOpenHandles 运行以查找泄漏。

如果我把它全部剥离出来:

方法 2 - 内存泄漏

import  Connection, createConnection  from 'mongoose';
import __MONGO_URI__ from './__MONGO_URI__';

let conn: Connection | null = null;

const getConnection: (MONGO_DB_NAME: string) => Promise<Connection> = async MONGO_DB_NAME => 
  if (conn == null) 
    conn = await createConnection(__MONGO_URI__, 
      dbName: MONGO_DB_NAME,
      bufferCommands: false,
      bufferMaxEntries: 0,
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true
    );
  
  return conn;
;

const MONGO_DB_NAME = 'mongo-test';
let db: Connection;

describe('mongo - connection test to ensure setup teardown', () => 
  beforeAll(async () => 
    db = await getConnection(MONGO_DB_NAME);
    console.log('db = ', db);
  );
  it('true = true', () => 
    expect(true).toBe(true);
  );
);

我还是有同样的问题

或者甚至这样做同样的问题:

方法 3 - 内存泄漏

import  Connection, createConnection  from 'mongoose';
import __MONGO_URI__ from './__MONGO_URI__';

let conn: Connection | null = null;

const getConnection: (MONGO_DB_NAME: string) => Promise<Connection | null> = MONGO_DB_NAME =>
  new Promise(resolve => 
    if (conn == null) 
      conn = createConnection(__MONGO_URI__, 
        dbName: MONGO_DB_NAME,
        bufferCommands: false,
        bufferMaxEntries: 0,
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useCreateIndex: true
      );
      conn.on('connected', () => 
        console.log('connected?');
        resolve(conn);
      );
    
    resolve(conn);
  );

const MONGO_DB_NAME = 'mongo-test';
let db: Connection;

describe('mongo - connection test to ensure setup teardown', () => 
  beforeAll(async () => 
    db = await getConnection(MONGO_DB_NAME);
    console.log('db = ', db);
  );
  it('true = true', () => 
    expect(true).toBe(true);
  );
);

这种方法也有同样的问题

方法 4 - 内存泄漏

import mongoose from 'mongoose';
import __MONGO_URI__ from './__MONGO_URI__';

let conn: typeof mongoose;

const getConnection: (MONGO_DB_NAME: string) => Promise<typeof mongoose> = async MONGO_DB_NAME => 
  if (!conn) 
    conn = await mongoose.connect(__MONGO_URI__, 
      dbName: MONGO_DB_NAME,
      // bufferCommands: false,
      // bufferMaxEntries: 0,
      // useNewUrlParser: true,
      // useUnifiedTopology: true,
      // useCreateIndex: true
    );
  
  return conn;
;

const MONGO_DB_NAME = 'mongo-test';
let db: typeof mongoose;

describe('mongo - connection test to ensure setup teardown', () => 
  beforeAll(async () => 
    db = await getConnection(MONGO_DB_NAME);
    console.log('db = ', db);
  );
  it('true = true', () => 
    expect(true).toBe(true);
  );
);

【问题讨论】:

那么,使用--runInBand --detectOpenHandles 运行的输出是什么? 没有错误输出吗? asyncdone 是一种反模式。应该是async 一个人。如果 dropDatabase 失败并且未达到关闭状态,则可能会出现问题。 --runInBand --detectOpenHandles 实际上并没有显示任何内容并且卡住并且没有退出测试 @EstusFlask 感谢您的反馈。导致内存泄漏的不是删除数据库或关闭连接。这是连接的实际创建。 createConnection 导致内存泄漏。如果我去掉代码只是为了shi。即使我删除了异步等待。 这不是连接本身导致泄漏,而是它被打开而不是关闭的事实。 Mongoose 连接肯定可以关闭,并且它们可以毫无问题地用于 Jest 测试。方法 2-4 在隔离问题方面没有任何价值,因为它们甚至不尝试关闭连接。按照我建议的方式尝试,去掉done,因为它有缺陷并且可以抑制错误。调试await conn.close() 真的被调用了。这是我在这里看到的唯一可能的问题。 【参考方案1】:

最终解决方案 - 感谢:@EstusFlask

getMongoConnection.ts

import  Connection, createConnection  from 'mongoose';
import __MONGO_URI__ from './__MONGO_URI__';

let conn: Connection | null = null;

const getConnection: (MONGO_DB_NAME: string) => Promise<Connection | null> = MONGO_DB_NAME =>
  new Promise(resolve => 
    if (conn == null) 
      conn = createConnection(__MONGO_URI__, 
        dbName: MONGO_DB_NAME,
        bufferCommands: false,
        bufferMaxEntries: 0,
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useCreateIndex: true
      );
      conn.on('connected', () => 
        resolve(conn);
      );
    
    resolve(conn);
  );

export default getConnection;

mytest.test.ts

import  Connection  from 'mongoose';
import getConnection from './getMongoConnection';

let db: Connection | null;

const MONGO_DB_NAME = 'mongo-test';
const collectionName = 'users';

describe('mongo - connection test to ensure setup teardown', () => 
  beforeAll(async () => 
    db = await getConnection(MONGO_DB_NAME);
  );
  afterAll(async () => 
    if (db) 
      await db.dropDatabase();
      await db.close();
    
  );
  it('should insert a doc into collection', async () => 
    if (db) 
      const users = db.collection(collectionName);

      const mockUser =  _id: 'some-user-id', name: 'John' ;
      await users.insertOne(mockUser);

      const insertedUser = await users.findOne( _id: 'some-user-id' );
      expect(insertedUser).toEqual(mockUser);
    
  );
);

【讨论】:

以上是关于开玩笑的猫鼬导致内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

常见的内存泄漏原因及解决方法

安卓内存分析——常见内存泄漏场景二

LeakCanary检测内存泄漏

Android内存泄漏

内存泄漏与溢出

JVM内存泄漏导致内存溢出(OOM)的场景