(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:
Jarosław Sadziński
2024-09-11 19:42:28 +02:00
parent 51acc9a8cc
commit 0ca70e9d43
6 changed files with 130 additions and 168 deletions

View File

@@ -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)

View File

@@ -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);
}
/**

View File

@@ -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);
});
}
/**

View File

@@ -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;"); }
}