Skip to content

Schema Cache Improvements #5612

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 24, 2019
Merged
1 change: 1 addition & 0 deletions spec/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Item": true,
"Container": true,
"equal": true,
"expectAsync": true,
"notEqual": true,
"it_only_db": true,
"it_exclude_dbs": true,
Expand Down
23 changes: 23 additions & 0 deletions spec/MongoStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,27 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
done();
});
});

it('getClass if exists', async () => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });

const schema = {
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
};

await adapter.createClass('MyClass', schema);
const myClassSchema = await adapter.getClass('MyClass');
expect(myClassSchema).toBeDefined();
});

it('getClass if not exists', async () => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(
undefined
);
});
});
27 changes: 27 additions & 0 deletions spec/PostgresStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,33 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
})
.catch(done);
});

it('getClass if exists', async () => {
const schema = {
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
};
await adapter.createClass('MyClass', schema);
const myClassSchema = await adapter.getClass('MyClass');
expect(myClassSchema).toBeDefined();
});

it('getClass if not exists', async () => {
const schema = {
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
};
await adapter.createClass('MyClass', schema);
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(
undefined
);
});
});

describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => {
Expand Down
167 changes: 167 additions & 0 deletions spec/RedisCacheAdapter.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter')
.default;
const Config = require('../lib/Config');

/*
To run this test part of the complete suite
set PARSE_SERVER_TEST_CACHE='redis'
Expand Down Expand Up @@ -163,3 +165,168 @@ describe_only(() => {
.then(done);
});
});

describe_only(() => {
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
})('Redis Performance', function() {
const cacheAdapter = new RedisCacheAdapter();
let getSpy;
let putSpy;

beforeEach(async () => {
await cacheAdapter.clear();
getSpy = spyOn(cacheAdapter, 'get').and.callThrough();
putSpy = spyOn(cacheAdapter, 'put').and.callThrough();
await reconfigureServer({
cacheAdapter,
enableSingleSchemaCache: true,
});
});

it('test new object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
expect(getSpy.calls.count()).toBe(4);
expect(putSpy.calls.count()).toBe(3);
});

it('test new object multiple fields', async () => {
const container = new Container({
dateField: new Date(),
arrayField: [],
numberField: 1,
stringField: 'hello',
booleanField: true,
});
await container.save();
expect(getSpy.calls.count()).toBe(4);
expect(putSpy.calls.count()).toBe(3);
});

it('test update existing fields', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();

getSpy.calls.reset();
putSpy.calls.reset();

object.set('foo', 'barz');
await object.save();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(0);
});

it('test add new field to existing object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();

getSpy.calls.reset();
putSpy.calls.reset();

object.set('new', 'barz');
await object.save();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(1);
});

it('test add multiple fields to existing object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();

getSpy.calls.reset();
putSpy.calls.reset();

object.set({
dateField: new Date(),
arrayField: [],
numberField: 1,
stringField: 'hello',
booleanField: true,
});
await object.save();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(1);
});

it('test query', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();

getSpy.calls.reset();
putSpy.calls.reset();

const query = new Parse.Query(TestObject);
await query.get(object.id);
expect(getSpy.calls.count()).toBe(2);
expect(putSpy.calls.count()).toBe(0);
});

it('test delete object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();

getSpy.calls.reset();
putSpy.calls.reset();

await object.destroy();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(0);
});

it('test schema update class', async () => {
const container = new Container();
await container.save();

getSpy.calls.reset();
putSpy.calls.reset();

const config = Config.get('test');
const schema = await config.database.loadSchema();
await schema.reloadData();

const levelPermissions = {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
protectedFields: { '*': [] },
};

await schema.updateClass(
'Container',
{
fooOne: { type: 'Number' },
fooTwo: { type: 'Array' },
fooThree: { type: 'Date' },
fooFour: { type: 'Object' },
fooFive: { type: 'Relation', targetClass: '_User' },
fooSix: { type: 'String' },
fooSeven: { type: 'Object' },
fooEight: { type: 'String' },
fooNine: { type: 'String' },
fooTeen: { type: 'Number' },
fooEleven: { type: 'String' },
fooTwelve: { type: 'String' },
fooThirteen: { type: 'String' },
fooFourteen: { type: 'String' },
fooFifteen: { type: 'String' },
fooSixteen: { type: 'String' },
fooEighteen: { type: 'String' },
fooNineteen: { type: 'String' },
},
levelPermissions,
{},
config.database
);
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(2);
});
});
41 changes: 41 additions & 0 deletions spec/Schema.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,47 @@ describe('SchemaController', () => {
.then(done)
.catch(done.fail);
});

it('setAllClasses return classes if cache fails', async () => {
const schema = await config.database.loadSchema();

spyOn(schema._cache, 'setAllClasses').and.callFake(() =>
Promise.reject('Oops!')
);
const errorSpy = spyOn(console, 'error').and.callFake(() => {});
const allSchema = await schema.setAllClasses();

expect(allSchema).toBeDefined();
expect(errorSpy).toHaveBeenCalledWith(
'Error saving schema to cache:',
'Oops!'
);
});

it('should not throw on null field types', async () => {
const schema = await config.database.loadSchema();
const result = await schema.enforceFieldExists(
'NewClass',
'fieldName',
null
);
expect(result).toBeUndefined();
});

it('ensureFields should throw when schema is not set', async () => {
const schema = await config.database.loadSchema();
try {
schema.ensureFields([
{
className: 'NewClass',
fieldName: 'fieldName',
type: 'String',
},
]);
} catch (e) {
expect(e.message).toBe('Could not add field fieldName');
}
});
});

describe('Class Level Permissions for requiredAuth', () => {
Expand Down
34 changes: 16 additions & 18 deletions spec/SchemaCache.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,12 @@ describe('SchemaCache', () => {
});
});

it('does not return all schemas after a single schema is stored', done => {
const schemaCache = new SchemaCache(cacheController);
const schema = {
className: 'Class1',
};
schemaCache
.setOneSchema(schema.className, schema)
.then(() => {
return schemaCache.getAllClasses();
})
.then(allSchemas => {
expect(allSchemas).toBeNull();
done();
});
});

it("doesn't persist cached data by default", done => {
const schemaCache = new SchemaCache(cacheController);
const schema = {
className: 'Class1',
};
schemaCache.setOneSchema(schema.className, schema).then(() => {
schemaCache.setAllClasses([schema]).then(() => {
const anotherSchemaCache = new SchemaCache(cacheController);
return anotherSchemaCache.getOneSchema(schema.className).then(schema => {
expect(schema).toBeNull();
Expand All @@ -68,12 +52,26 @@ describe('SchemaCache', () => {
const schema = {
className: 'Class1',
};
schemaCache.setOneSchema(schema.className, schema).then(() => {
schemaCache.setAllClasses([schema]).then(() => {
const anotherSchemaCache = new SchemaCache(cacheController, 5000, true);
return anotherSchemaCache.getOneSchema(schema.className).then(schema => {
expect(schema).not.toBeNull();
done();
});
});
});

it('should not store if ttl is null', async () => {
const ttl = null;
const schemaCache = new SchemaCache(cacheController, ttl);
expect(await schemaCache.getAllClasses()).toBeNull();
expect(await schemaCache.setAllClasses()).toBeNull();
expect(await schemaCache.getOneSchema()).toBeNull();
});

it('should convert string ttl to number', async () => {
const ttl = '5000';
const schemaCache = new SchemaCache(cacheController, ttl);
expect(schemaCache.ttl).toBe(5000);
});
});
1 change: 0 additions & 1 deletion src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,6 @@ class DatabaseController {
: schemaController.validatePermission(className, aclGroup, 'create')
)
.then(() => schemaController.enforceClassExists(className))
.then(() => schemaController.reloadData())
.then(() => schemaController.getOneSchema(className, true))
.then(schema => {
transformAuthData(className, object, schema);
Expand Down
Loading