Skip to content

Commit b444eff

Browse files
authored
Merge pull request #391 from Urigo/memory-leak-testing
Add Memory Leak tests
2 parents 54af7cd + fec59e2 commit b444eff

File tree

5 files changed

+317
-58
lines changed

5 files changed

+317
-58
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"graphql": "^14.1.1"
4242
},
4343
"devDependencies": {
44+
"@types/leakage": "0.4.0",
4445
"apollo-cache-inmemory": "1.5.1",
4546
"apollo-client": "2.5.1",
4647
"apollo-link-schema": "1.2.2",

packages/core/tests/graphql-module.spec.ts

Lines changed: 229 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ import { Injectable, Inject, InjectFunction, Injector, ProviderScope, Dependency
1717
import { SchemaLink } from 'apollo-link-schema';
1818
import { ApolloClient } from 'apollo-client';
1919
import { InMemoryCache } from 'apollo-cache-inmemory';
20-
import { EventEmitter } from 'events';
2120
import { KeyValueCache } from 'apollo-server-caching';
21+
import { iterate } from 'leakage';
22+
import { EventEmitter } from 'events';
23+
import { writeFile } from 'fs';
2224

25+
jest.setTimeout(60000 * 10);
2326
describe('GraphQLModule', () => {
2427
// A
2528
@Injectable()
@@ -1140,7 +1143,7 @@ describe('GraphQLModule', () => {
11401143
isDirty: (root, args, context, info) => !!info.schema['__DIRTY__'],
11411144
},
11421145
},
1143-
middleware: ({schema}) => { schema['__DIRTY__'] = true; return { schema }; },
1146+
middleware: ({ schema }) => { schema['__DIRTY__'] = true; return { schema }; },
11441147
});
11451148
const { schema, context } = new GraphQLModule({
11461149
imports: [
@@ -1262,7 +1265,7 @@ describe('GraphQLModule', () => {
12621265
@Injectable()
12631266
class TestDataSourceAPI {
12641267
cache: KeyValueCache;
1265-
initialize({ cache }: { cache: KeyValueCache}) {
1268+
initialize({ cache }: { cache: KeyValueCache }) {
12661269
this.cache = cache;
12671270
}
12681271
}
@@ -1297,7 +1300,7 @@ describe('GraphQLModule', () => {
12971300
class TestDataSourceAPI {
12981301
context: any;
12991302
cache: KeyValueCache;
1300-
public initialize({ context, cache }: { context: any, cache: KeyValueCache}) {
1303+
public initialize({ context, cache }: { context: any, cache: KeyValueCache }) {
13011304
this.context = context;
13021305
this.cache = cache;
13031306
}
@@ -1467,14 +1470,14 @@ describe('GraphQLModule', () => {
14671470
it('should throw an error if promises are used without schemaAsync', async () => {
14681471
const MyAsyncModule = new GraphQLModule({
14691472
typeDefs: async () => `type Query { test: Boolean }`,
1470-
resolvers: async () => ({ Query: { test: () => true }}),
1473+
resolvers: async () => ({ Query: { test: () => true } }),
14711474
});
14721475
expect(() => MyAsyncModule.schema).toThrow();
14731476
});
14741477
it('should support promises with schemaAsync', async () => {
14751478
const { schemaAsync } = new GraphQLModule({
14761479
typeDefs: async () => `type Query { test: Boolean }`,
1477-
resolvers: async () => ({ Query: { test: () => true }}),
1480+
resolvers: async () => ({ Query: { test: () => true } }),
14781481
});
14791482
const result = await execute({
14801483
schema: await schemaAsync,
@@ -1540,60 +1543,233 @@ describe('GraphQLModule', () => {
15401543
expect(result.data['qux']).toBe('QUX');
15411544
});
15421545
it('should not have _onceFinishListeners on response object', async (done) => {
1543-
let counter = 0;
1544-
@Injectable({
1545-
scope: ProviderScope.Session,
1546-
})
1547-
class FooProvider implements OnResponse {
1548-
onResponse() {
1549-
counter++;
1546+
let counter = 0;
1547+
@Injectable({
1548+
scope: ProviderScope.Session,
1549+
})
1550+
class FooProvider implements OnResponse {
1551+
onResponse() {
1552+
counter++;
1553+
}
1554+
getCounter() {
1555+
return counter;
1556+
}
1557+
}
1558+
1559+
const module = new GraphQLModule({
1560+
typeDefs: gql`
1561+
type Query {
1562+
foo: Int
15501563
}
1551-
getCounter() {
1552-
return counter;
1564+
`,
1565+
resolvers: {
1566+
Query: {
1567+
foo: (_, __, { injector }) => injector.get(FooProvider).getCounter(),
1568+
},
1569+
},
1570+
providers: [
1571+
FooProvider,
1572+
],
1573+
});
1574+
const session = createMockSession({});
1575+
const { data } = await execute({
1576+
schema: module.schema,
1577+
contextValue: session,
1578+
document: gql`query { foo }`,
1579+
});
1580+
// Result
1581+
expect(data.foo).toBe(0);
1582+
// Before onResponse
1583+
expect(counter).toBe(0);
1584+
await session.res.emit('finish');
1585+
// After onResponse
1586+
expect(counter).toBe(1);
1587+
// Check if the listener is triggered again
1588+
session.res.once('finish', () => {
1589+
setTimeout(() => {
1590+
expect(counter).toBe(1);
1591+
// Response object must be cleared
1592+
expect(session.res['_onceFinishListeners']).toBeUndefined();
1593+
expect(module.injector.hasSessionInjector(session)).toBeFalsy();
1594+
expect(module['_sessionContext$Map'].has(session)).toBeFalsy();
1595+
done();
1596+
}, 1000);
1597+
});
1598+
session.res.emit('finish');
1599+
});
1600+
it.skip('should not have memory leak over multiple sessions with session-scoped providers', done => {
1601+
1602+
@Injectable({
1603+
scope: ProviderScope.Session,
1604+
})
1605+
class AProvider {
1606+
constructor(private moduleSessionInfo: ModuleSessionInfo) { }
1607+
getLoadLength() {
1608+
return this.moduleSessionInfo.session.hugeLoad.length;
1609+
}
1610+
}
1611+
const moduleA = new GraphQLModule({
1612+
typeDefs: gql`
1613+
type Query {
1614+
aLoadLength: Int
15531615
}
1616+
`,
1617+
resolvers: {
1618+
Query: {
1619+
aLoadLength: (_, __, { injector }) => injector.get(AProvider).getLoadLength(),
1620+
},
1621+
},
1622+
providers: [
1623+
AProvider,
1624+
],
1625+
});
1626+
@Injectable({
1627+
scope: ProviderScope.Session,
1628+
})
1629+
class BProvider {
1630+
constructor(private moduleSessionInfo: ModuleSessionInfo) { }
1631+
getLoadLength() {
1632+
return this.moduleSessionInfo.session.hugeLoad.length;
15541633
}
1634+
getB() {
1635+
return 'B';
1636+
}
1637+
}
1638+
const moduleB = new GraphQLModule({
1639+
typeDefs: gql`
1640+
type Query {
1641+
aLoadLength: Int
1642+
}
1643+
`,
1644+
resolvers: {
1645+
Query: {
1646+
bLoadLength: (_, __, { injector }) => injector.get(BProvider).getLoadLength(),
1647+
},
1648+
},
1649+
providers: [
1650+
BProvider,
1651+
],
1652+
});
1653+
const { schema } = new GraphQLModule({
1654+
imports: [
1655+
moduleA,
1656+
moduleB,
1657+
],
1658+
});
15551659

1556-
const module = new GraphQLModule({
1557-
typeDefs: gql`
1558-
type Query {
1559-
foo: Int
1560-
}
1561-
`,
1562-
resolvers: {
1563-
Query: {
1564-
foo: (_, __, { injector }) => injector.get(FooProvider).getCounter(),
1565-
},
1660+
let counter = 0;
1661+
iterate.async(async () => {
1662+
// tslint:disable-next-line: no-console
1663+
console.log(`Iteration: ${counter} start`);
1664+
await execute({
1665+
schema,
1666+
contextValue: {
1667+
hugeLoad: new Array(1000).fill(1000),
15661668
},
1567-
providers: [
1568-
FooProvider,
1569-
],
1669+
document: gql`{ aLoadLength bLoadLength }`,
15701670
});
1571-
const session = createMockSession({});
1671+
// tslint:disable-next-line: no-console
1672+
console.log(`Iteration: ${counter} end`);
1673+
counter++;
1674+
}).then(done).catch(done.fail);
1675+
1676+
});
1677+
it('should not memory leak over multiple sessions (not collected by GC but emitting finish event) with session-scoped providers', done => {
1678+
1679+
let counter = 0;
1680+
@Injectable({
1681+
scope: ProviderScope.Session,
1682+
})
1683+
class AProvider {
1684+
aHugeLoad = new Array(1000).fill(1000);
1685+
1686+
constructor(private moduleSessionInfo: ModuleSessionInfo) {
1687+
counter++;
1688+
}
1689+
getLoadLength() {
1690+
return this.moduleSessionInfo.session.hugeLoad.length;
1691+
}
1692+
getALoadLength() {
1693+
return this.aHugeLoad.length;
1694+
}
1695+
}
1696+
const moduleA = new GraphQLModule({
1697+
typeDefs: gql`
1698+
type Query {
1699+
aLoadLength: Int
1700+
abLoadLength: Int
1701+
}
1702+
`,
1703+
resolvers: {
1704+
Query: {
1705+
aLoadLength: (_, __, { injector }) => injector.get(AProvider).getALoadLength(),
1706+
abLoadLength: (_, __, { injector }) => injector.get(AProvider).getLoadLength(),
1707+
},
1708+
},
1709+
providers: [
1710+
AProvider,
1711+
],
1712+
});
1713+
@Injectable({
1714+
scope: ProviderScope.Session,
1715+
})
1716+
class BProvider {
1717+
bHugeLoad = new Array(1000).fill(1000);
1718+
constructor(private moduleSessionInfo: ModuleSessionInfo) { }
1719+
getLoadLength() {
1720+
return this.moduleSessionInfo.session.hugeLoad.length;
1721+
}
1722+
getBLoadLength() {
1723+
return this.bHugeLoad.length;
1724+
}
1725+
}
1726+
const moduleB = new GraphQLModule({
1727+
typeDefs: gql`
1728+
type Query {
1729+
bLoadLength: Int
1730+
baLoadLength: Int
1731+
}
1732+
`,
1733+
resolvers: {
1734+
Query: {
1735+
bLoadLength: (_, __, { injector }) => injector.get(BProvider).getBLoadLength(),
1736+
baLoadLength: (_, __, { injector }) => injector.get(BProvider).getLoadLength(),
1737+
},
1738+
},
1739+
providers: [
1740+
BProvider,
1741+
],
1742+
});
1743+
const { schema } = new GraphQLModule({
1744+
imports: [
1745+
moduleA,
1746+
moduleB,
1747+
],
1748+
});
1749+
const mockRequests: Array<MockSession<{ hugeLoad: number[] }>> = [];
1750+
for (let i = 0; i < 1000; i++) {
1751+
mockRequests.push(createMockSession({ hugeLoad: new Array(1000).fill(1000) }));
1752+
}
1753+
iterate.async(() => new Promise(async resolve => {
1754+
// tslint:disable-next-line: no-console
1755+
console.log(`Iteration started`);
1756+
const mockRequest = mockRequests[Math.floor(Math.random() * mockRequests.length)];
15721757
const { data } = await execute({
1573-
schema: module.schema,
1574-
contextValue: session,
1575-
document: gql`query { foo }`,
1758+
schema,
1759+
contextValue: mockRequest,
1760+
document: gql`{ aLoadLength bLoadLength abLoadLength baLoadLength }`,
15761761
});
1577-
// Result
1578-
expect(data.foo).toBe(0);
1579-
// Before onResponse
1580-
expect(counter).toBe(0);
1581-
session.res.emit('finish');
1582-
// After onResponse
1583-
expect(counter).toBe(1);
1584-
// Check if the listener is triggered again
1585-
session.res.emit('finish');
1586-
expect(counter).toBe(1);
1587-
setTimeout(() => {
1588-
try {
1589-
// Response object must be cleared
1590-
expect(session.res['_onceFinishListeners']).toBeUndefined();
1591-
expect(module.injector.hasSessionInjector(session)).toBeFalsy();
1592-
expect(module['_sessionContext$Map'].has(session)).toBeFalsy();
1593-
done();
1594-
} catch (e) {
1595-
done.fail(e);
1596-
}
1597-
}, 1000);
1762+
mockRequest.res.emit('finish');
1763+
expect(data.aLoadLength).toBe(1000);
1764+
expect(data.bLoadLength).toBe(1000);
1765+
expect(data.abLoadLength).toBe(1000);
1766+
expect(data.baLoadLength).toBe(1000);
1767+
// tslint:disable-next-line: no-console
1768+
console.log(counter);
1769+
resolve();
1770+
})).then(() => {
1771+
done();
1772+
}).catch(done.fail);
1773+
15981774
});
15991775
});

packages/di/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"reflect-metadata": "^0.1.12"
3939
},
4040
"devDependencies": {
41+
"leakage": "0.4.0",
4142
"jest": "24.7.0",
4243
"reflect-metadata": "0.1.13",
4344
"tslint": "5.15.0",

0 commit comments

Comments
 (0)