mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Fixing lock issues and reverting back to single connection.
Summary: Removing `createNewConnection` method that was used in tests to create a "scoped" version of `HomeDbManager`. Currently this won't work as there are many methods (like `Users.findOne`) that are using the default (global) connection. Additionally `HomeDBManger` had couple of bugs that were causing locks, which manifested themselves in postgresql tests (that are not serializing transactions). Repository methods like `Users.findOne` or `user.save()`, even when wrapped in transaction were using a separate connection from the pool (and a separate transaction). Some tests in `UsersManager` are still skipped or refactored, as sinon's `fakeTimers` doesn't work well with postgresql driver (which is using `setTimout` a lot). Date mappings in `User` entity were fixed, they were using `SQLite` configuration only, which caused problems with postgresql database. Test Plan: Refactored. Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4342
This commit is contained in:
@@ -26,10 +26,10 @@ export class User extends BaseEntity {
|
||||
@Column({name: 'picture', type: String, nullable: true})
|
||||
public picture: string | null;
|
||||
|
||||
@Column({name: 'first_login_at', type: Date, nullable: true})
|
||||
@Column({name: 'first_login_at', type: nativeValues.dateTimeType, nullable: true})
|
||||
public firstLoginAt: Date | null;
|
||||
|
||||
@Column({name: 'last_connection_at', type: Date, nullable: true})
|
||||
@Column({name: 'last_connection_at', type: nativeValues.dateTimeType, nullable: true})
|
||||
public lastConnectionAt: Date | null;
|
||||
|
||||
@OneToOne(type => Organization, organization => organization.owner)
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
readJson
|
||||
} from 'app/gen-server/sqlUtils';
|
||||
import {appSettings} from 'app/server/lib/AppSettings';
|
||||
import {createNewConnection, getOrCreateConnection} from 'app/server/lib/dbUtils';
|
||||
import {getOrCreateConnection} from 'app/server/lib/dbUtils';
|
||||
import {makeId} from 'app/server/lib/idUtils';
|
||||
import log from 'app/server/lib/log';
|
||||
import {Permit} from 'app/server/lib/Permit';
|
||||
@@ -70,7 +70,6 @@ import {
|
||||
Brackets,
|
||||
Connection,
|
||||
DatabaseType,
|
||||
DataSourceOptions,
|
||||
EntityManager,
|
||||
ObjectLiteral,
|
||||
SelectQueryBuilder,
|
||||
@@ -354,8 +353,8 @@ export class HomeDBManager extends EventEmitter {
|
||||
this._connection = await getOrCreateConnection();
|
||||
}
|
||||
|
||||
public async createNewConnection(overrideConf?: Partial<DataSourceOptions>): Promise<void> {
|
||||
this._connection = await createNewConnection(overrideConf);
|
||||
public connectTo(connection: Connection) {
|
||||
this._connection = connection;
|
||||
}
|
||||
|
||||
// make sure special users and workspaces are available
|
||||
@@ -458,7 +457,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
* @see UsersManager.prototype.ensureExternalUser
|
||||
*/
|
||||
public async ensureExternalUser(profile: UserProfile) {
|
||||
return this._usersManager.ensureExternalUser(profile);
|
||||
return await this._usersManager.ensureExternalUser(profile);
|
||||
}
|
||||
|
||||
public async updateUser(userId: number, props: UserProfileChange) {
|
||||
@@ -491,7 +490,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
* Find a user by email. Don't create the user if it doesn't already exist.
|
||||
*/
|
||||
public async getExistingUserByLogin(email: string, manager?: EntityManager): Promise<User|undefined> {
|
||||
return this._usersManager.getExistingUserByLogin(email, manager);
|
||||
return await this._usersManager.getExistingUserByLogin(email, manager);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -223,7 +223,7 @@ export class UsersManager {
|
||||
});
|
||||
// No need to survey this user.
|
||||
newUser.isFirstTimeUser = false;
|
||||
await newUser.save();
|
||||
await manager.save(newUser);
|
||||
} else {
|
||||
// Else update profile and login information from external profile.
|
||||
let updated = false;
|
||||
@@ -277,7 +277,7 @@ export class UsersManager {
|
||||
if (!props.isFirstTimeUser) { isWelcomed = true; }
|
||||
}
|
||||
if (needsSave) {
|
||||
await user.save();
|
||||
await manager.save(user);
|
||||
}
|
||||
});
|
||||
return { user, isWelcomed };
|
||||
@@ -285,12 +285,12 @@ export class UsersManager {
|
||||
|
||||
// TODO: rather use the updateUser() method, if that makes sense?
|
||||
public async updateUserOptions(userId: number, props: Partial<UserOptions>) {
|
||||
const user = await User.findOne({where: {id: userId}});
|
||||
if (!user) { throw new ApiError("unable to find user", 400); }
|
||||
|
||||
const newOptions = {...(user.options ?? {}), ...props};
|
||||
user.options = newOptions;
|
||||
await user.save();
|
||||
await this._runInTransaction(undefined, async manager => {
|
||||
const user = await manager.findOne(User, {where: {id: userId}});
|
||||
if (!user) { throw new ApiError("unable to find user", 400); }
|
||||
user.options = {...(user.options ?? {}), ...props};
|
||||
await manager.save(user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,43 +55,33 @@ export function getConnectionName() {
|
||||
*/
|
||||
const connectionMutex = new Mutex();
|
||||
|
||||
async function buildConnection(overrideConf?: Partial<DataSourceOptions>) {
|
||||
const settings = getTypeORMSettings(overrideConf);
|
||||
const connection = await createConnection(settings);
|
||||
// When using Sqlite, set a busy timeout of 3s to tolerate a little
|
||||
// interference from connections made by tests. Logging doesn't show
|
||||
// any particularly slow queries, but bad luck is possible.
|
||||
// This doesn't affect when Postgres is in use. It also doesn't have
|
||||
// any impact when there is a single connection to the db, as is the
|
||||
// case when Grist is run as a single process.
|
||||
if (connection.driver.options.type === 'sqlite') {
|
||||
await connection.query('PRAGMA busy_timeout = 3000');
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
export async function getOrCreateConnection(): Promise<Connection> {
|
||||
return connectionMutex.runExclusive(async () => {
|
||||
return connectionMutex.runExclusive(async() => {
|
||||
try {
|
||||
// If multiple servers are started within the same process, we
|
||||
// share the database connection. This saves locking trouble
|
||||
// with Sqlite.
|
||||
return getConnection(getConnectionName());
|
||||
const connection = getConnection();
|
||||
return connection;
|
||||
} catch (e) {
|
||||
if (!String(e).match(/ConnectionNotFoundError/)) {
|
||||
throw e;
|
||||
}
|
||||
return buildConnection();
|
||||
const connection = await createConnection(getTypeORMSettings());
|
||||
// When using Sqlite, set a busy timeout of 3s to tolerate a little
|
||||
// interference from connections made by tests. Logging doesn't show
|
||||
// any particularly slow queries, but bad luck is possible.
|
||||
// This doesn't affect when Postgres is in use. It also doesn't have
|
||||
// any impact when there is a single connection to the db, as is the
|
||||
// case when Grist is run as a single process.
|
||||
if (connection.driver.options.type === 'sqlite') {
|
||||
await connection.query('PRAGMA busy_timeout = 3000');
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function createNewConnection(overrideConf?: Partial<DataSourceOptions>): Promise<Connection> {
|
||||
return connectionMutex.runExclusive(async () => {
|
||||
return buildConnection(overrideConf);
|
||||
});
|
||||
}
|
||||
|
||||
export async function runMigrations(connection: Connection) {
|
||||
// on SQLite, migrations fail if we don't temporarily disable foreign key
|
||||
// constraint checking. This is because for sqlite typeorm copies each
|
||||
@@ -100,9 +90,7 @@ export async function runMigrations(connection: Connection) {
|
||||
// transaction, or it has no effect.
|
||||
const sqlite = connection.driver.options.type === 'sqlite';
|
||||
if (sqlite) { await connection.query("PRAGMA foreign_keys = OFF;"); }
|
||||
await connection.transaction(async tr => {
|
||||
await tr.connection.runMigrations();
|
||||
});
|
||||
await connection.runMigrations({ transaction: "all" });
|
||||
if (sqlite) { await connection.query("PRAGMA foreign_keys = ON;"); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user