1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
This commit is contained in:
Laurent Cozic 2024-05-10 13:48:54 +01:00
parent 3813448b1e
commit 087a4b28b1
17 changed files with 180 additions and 96 deletions

View File

@ -5,7 +5,8 @@
version: '3' version: '3'
services: services:
db:
postgres_master:
image: postgres:16 image: postgres:16
command: postgres -c work_mem=100000 command: postgres -c work_mem=100000
ports: ports:
@ -14,6 +15,34 @@ services:
- POSTGRES_PASSWORD=joplin - POSTGRES_PASSWORD=joplin
- POSTGRES_USER=joplin - POSTGRES_USER=joplin
- POSTGRES_DB=joplin - POSTGRES_DB=joplin
# - POSTGRESQL_PGAUDIT_LOG=READ,WRITE
- POSTGRESQL_LOG_HOSTNAME=true
- POSTGRESQL_REPLICATION_MODE=master
- POSTGRESQL_REPLICATION_USER=repl_user
- POSTGRESQL_REPLICATION_PASSWORD=repl_password
postgres_slave:
image: postgres:16
command: postgres -c work_mem=100000
ports:
- "5433:5432"
depends_on:
- postgres_master
environment:
- POSTGRES_PASSWORD=joplin
- POSTGRES_USER=joplin
- POSTGRES_DB=joplin
# - POSTGRESQL_PGAUDIT_LOG=READ,WRITE
- POSTGRESQL_MASTER_HOST=postgresql-master
- POSTGRESQL_LOG_HOSTNAME=true
- POSTGRESQL_REPLICATION_MODE=slave
- POSTGRESQL_REPLICATION_USER=repl_user
- POSTGRESQL_REPLICATION_PASSWORD=repl_password
- POSTGRESQL_MASTER_PORT_NUMBER=5432
# Use this to specify additional Postgres # Use this to specify additional Postgres
# config parameters: # config parameters:

View File

@ -20,8 +20,8 @@ export function runningInDocker(): boolean {
return runningInDocker_; return runningInDocker_;
} }
function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables, replica: boolean): string { function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables, slave: boolean): string {
const postgresHost = replica ? env.REPLICA_POSTGRES_HOST : env.POSTGRES_HOST; const postgresHost = slave ? env.SLAVE_POSTGRES_HOST : env.POSTGRES_HOST;
if (postgresHost) { if (postgresHost) {
// When running within Docker, the app localhost is different from the // When running within Docker, the app localhost is different from the
@ -44,7 +44,7 @@ export const fullVersionString = (config: Config) => {
return output.join(' '); return output.join(' ');
}; };
function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables, replica: boolean): DatabaseConfig { function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables, slave: boolean): DatabaseConfig {
const baseConfig: DatabaseConfig = { const baseConfig: DatabaseConfig = {
client: DatabaseConfigClient.Null, client: DatabaseConfigClient.Null,
name: '', name: '',
@ -61,16 +61,16 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables, repl
if (env.POSTGRES_CONNECTION_STRING) { if (env.POSTGRES_CONNECTION_STRING) {
return { return {
...databaseConfig, ...databaseConfig,
connectionString: replica ? env.REPLICA_POSTGRES_CONNECTION_STRING : env.POSTGRES_CONNECTION_STRING, connectionString: slave ? env.SLAVE_POSTGRES_CONNECTION_STRING : env.POSTGRES_CONNECTION_STRING,
}; };
} else { } else {
return { return {
...databaseConfig, ...databaseConfig,
name: replica ? env.REPLICA_POSTGRES_DATABASE : env.POSTGRES_DATABASE, name: slave ? env.SLAVE_POSTGRES_DATABASE : env.POSTGRES_DATABASE,
user: replica ? env.REPLICA_POSTGRES_USER : env.POSTGRES_USER, user: slave ? env.SLAVE_POSTGRES_USER : env.POSTGRES_USER,
password: replica ? env.REPLICA_POSTGRES_PASSWORD : env.POSTGRES_PASSWORD, password: slave ? env.SLAVE_POSTGRES_PASSWORD : env.POSTGRES_PASSWORD,
port: replica ? env.REPLICA_POSTGRES_PORT : env.POSTGRES_PORT, port: slave ? env.SLAVE_POSTGRES_PORT : env.POSTGRES_PORT,
host: databaseHostFromEnv(runningInDocker, env, replica) || 'localhost', host: databaseHostFromEnv(runningInDocker, env, slave) || 'localhost',
}; };
} }
} }
@ -78,7 +78,7 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables, repl
return { return {
...baseConfig, ...baseConfig,
client: DatabaseConfigClient.SQLite, client: DatabaseConfigClient.SQLite,
name: env.SQLITE_DATABASE, name: slave ? env.SLAVE_SQLITE_DATABASE : env.SQLITE_DATABASE,
asyncStackTraces: true, asyncStackTraces: true,
}; };
} }
@ -173,7 +173,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
tempDir: `${rootDir}/temp`, tempDir: `${rootDir}/temp`,
logDir: `${rootDir}/logs`, logDir: `${rootDir}/logs`,
database: dbConfig, database: dbConfig,
databaseReplica: env.DB_USE_REPLICA ? databaseConfigFromEnv(runningInDocker_, env, true) : dbConfig, databaseSlave: env.DB_USE_SLAVE ? databaseConfigFromEnv(runningInDocker_, env, true) : dbConfig,
mailer: mailerConfigFromEnv(env), mailer: mailerConfigFromEnv(env),
stripe: stripeConfigFromEnv(stripePublicConfig, env), stripe: stripeConfigFromEnv(stripePublicConfig, env),
port: appPort, port: appPort,

View File

@ -59,7 +59,7 @@ const defaultEnvValues: EnvVariables = {
DB_SLOW_QUERY_LOG_MIN_DURATION: 1000, DB_SLOW_QUERY_LOG_MIN_DURATION: 1000,
DB_AUTO_MIGRATION: true, DB_AUTO_MIGRATION: true,
DB_ALLOW_INCOMPLETE_MIGRATIONS: false, DB_ALLOW_INCOMPLETE_MIGRATIONS: false,
DB_USE_REPLICA: false, DB_USE_SLAVE: false,
POSTGRES_PASSWORD: 'joplin', POSTGRES_PASSWORD: 'joplin',
POSTGRES_DATABASE: 'joplin', POSTGRES_DATABASE: 'joplin',
@ -68,15 +68,16 @@ const defaultEnvValues: EnvVariables = {
POSTGRES_PORT: 5432, POSTGRES_PORT: 5432,
POSTGRES_CONNECTION_STRING: '', POSTGRES_CONNECTION_STRING: '',
REPLICA_POSTGRES_PASSWORD: 'joplin', SLAVE_POSTGRES_PASSWORD: 'joplin',
REPLICA_POSTGRES_DATABASE: 'joplin', SLAVE_POSTGRES_DATABASE: 'joplin',
REPLICA_POSTGRES_USER: 'joplin', SLAVE_POSTGRES_USER: 'joplin',
REPLICA_POSTGRES_HOST: '', SLAVE_POSTGRES_HOST: '',
REPLICA_POSTGRES_PORT: 5432, SLAVE_POSTGRES_PORT: 5432,
REPLICA_POSTGRES_CONNECTION_STRING: '', SLAVE_POSTGRES_CONNECTION_STRING: '',
// This must be the full path to the database file // This must be the full path to the database file
SQLITE_DATABASE: '', SQLITE_DATABASE: '',
SLAVE_SQLITE_DATABASE: '',
// ================================================== // ==================================================
// Content driver config // Content driver config
@ -165,7 +166,7 @@ export interface EnvVariables {
DB_SLOW_QUERY_LOG_MIN_DURATION: number; DB_SLOW_QUERY_LOG_MIN_DURATION: number;
DB_AUTO_MIGRATION: boolean; DB_AUTO_MIGRATION: boolean;
DB_ALLOW_INCOMPLETE_MIGRATIONS: boolean; DB_ALLOW_INCOMPLETE_MIGRATIONS: boolean;
DB_USE_REPLICA: boolean; DB_USE_SLAVE: boolean;
POSTGRES_PASSWORD: string; POSTGRES_PASSWORD: string;
POSTGRES_DATABASE: string; POSTGRES_DATABASE: string;
@ -174,14 +175,15 @@ export interface EnvVariables {
POSTGRES_PORT: number; POSTGRES_PORT: number;
POSTGRES_CONNECTION_STRING: string; POSTGRES_CONNECTION_STRING: string;
REPLICA_POSTGRES_PASSWORD: string; SLAVE_POSTGRES_PASSWORD: string;
REPLICA_POSTGRES_DATABASE: string; SLAVE_POSTGRES_DATABASE: string;
REPLICA_POSTGRES_USER: string; SLAVE_POSTGRES_USER: string;
REPLICA_POSTGRES_HOST: string; SLAVE_POSTGRES_HOST: string;
REPLICA_POSTGRES_PORT: number; SLAVE_POSTGRES_PORT: number;
REPLICA_POSTGRES_CONNECTION_STRING: string; SLAVE_POSTGRES_CONNECTION_STRING: string;
SQLITE_DATABASE: string; SQLITE_DATABASE: string;
SLAVE_SQLITE_DATABASE: string;
STORAGE_DRIVER: string; STORAGE_DRIVER: string;
STORAGE_DRIVER_FALLBACK: string; STORAGE_DRIVER_FALLBACK: string;

View File

@ -64,15 +64,15 @@ export default abstract class BaseModel<T> {
private defaultFields_: string[] = []; private defaultFields_: string[] = [];
private db_: DbConnection; private db_: DbConnection;
private dbReplica_: DbConnection; private dbSlave_: DbConnection;
private transactionHandler_: TransactionHandler; private transactionHandler_: TransactionHandler;
private modelFactory_: NewModelFactoryHandler; private modelFactory_: NewModelFactoryHandler;
private config_: Config; private config_: Config;
private savePoints_: SavePoint[] = []; private savePoints_: SavePoint[] = [];
public constructor(db: DbConnection, dbReplica: DbConnection, modelFactory: NewModelFactoryHandler, config: Config) { public constructor(db: DbConnection, dbSlave: DbConnection, modelFactory: NewModelFactoryHandler, config: Config) {
this.db_ = db; this.db_ = db;
this.dbReplica_ = dbReplica; this.dbSlave_ = dbSlave;
this.modelFactory_ = modelFactory; this.modelFactory_ = modelFactory;
this.config_ = config; this.config_ = config;
@ -115,8 +115,8 @@ export default abstract class BaseModel<T> {
return this.db_; return this.db_;
} }
public get dbReplica(): DbConnection { public get dbSlave(): DbConnection {
return this.dbReplica_; return this.dbSlave_;
} }
protected get defaultFields(): string[] { protected get defaultFields(): string[] {

View File

@ -343,4 +343,18 @@ describe('ChangeModel', () => {
expect('jopItem' in result.items[0]).toBe(false); expect('jopItem' in result.items[0]).toBe(false);
}); });
// test('should use slave', async () => {
// const { session, user } = await createUserAndSession(1, true);
// const changeModel = models().change();
// const item1 = await createFolder(session.id, { title: 'folder' });
// // {
// // const changes = (await changeModel.delta(user.id)).items;
// // expect(changes.length).toBe(1);
// // expect(changes[0].item_id).toBe(item1.id);
// // expect(changes[0].type).toBe(ChangeType.Create);
// // }
// });
}); });

View File

@ -57,8 +57,8 @@ export default class ChangeModel extends BaseModel<Change> {
public deltaIncludesItems_: boolean; public deltaIncludesItems_: boolean;
public constructor(db: DbConnection, dbReplica: DbConnection, modelFactory: NewModelFactoryHandler, config: Config) { public constructor(db: DbConnection, dbSlave: DbConnection, modelFactory: NewModelFactoryHandler, config: Config) {
super(db, dbReplica, modelFactory, config); super(db, dbSlave, modelFactory, config);
this.deltaIncludesItems_ = config.DELTA_INCLUDES_ITEMS; this.deltaIncludesItems_ = config.DELTA_INCLUDES_ITEMS;
} }
@ -199,8 +199,8 @@ export default class ChangeModel extends BaseModel<Change> {
if (!doCountQuery) { if (!doCountQuery) {
finalParams.push(limit); finalParams.push(limit);
if (isPostgres(this.dbReplica)) { if (isPostgres(this.dbSlave)) {
query = this.dbReplica.raw(` query = this.dbSlave.raw(`
WITH cte1 AS MATERIALIZED ( WITH cte1 AS MATERIALIZED (
${subQuery1} ${subQuery1}
) )
@ -214,7 +214,7 @@ export default class ChangeModel extends BaseModel<Change> {
LIMIT ? LIMIT ?
`, finalParams); `, finalParams);
} else { } else {
query = this.dbReplica.raw(` query = this.dbSlave.raw(`
SELECT ${fieldsSql} FROM (${subQuery1}) as sub1 SELECT ${fieldsSql} FROM (${subQuery1}) as sub1
UNION ALL UNION ALL
SELECT ${fieldsSql} FROM (${subQuery2}) as sub2 SELECT ${fieldsSql} FROM (${subQuery2}) as sub2
@ -223,7 +223,7 @@ export default class ChangeModel extends BaseModel<Change> {
`, finalParams); `, finalParams);
} }
} else { } else {
query = this.dbReplica.raw(` query = this.dbSlave.raw(`
SELECT count(*) as total SELECT count(*) as total
FROM ( FROM (
(${subQuery1}) (${subQuery1})

View File

@ -1,4 +1,4 @@
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, createItemTree, createResource, createNote, createItemTree3, db, tempDir, expectNotThrow, expectHttpError, dbReplica } from '../utils/testing/testUtils'; import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, createItemTree, createResource, createNote, createItemTree3, db, tempDir, expectNotThrow, expectHttpError, dbSlave } from '../utils/testing/testUtils';
import { shareFolderWithUser } from '../utils/testing/shareApiUtils'; import { shareFolderWithUser } from '../utils/testing/shareApiUtils';
import { resourceBlobPath } from '../utils/joplinUtils'; import { resourceBlobPath } from '../utils/joplinUtils';
import newModelFactory from './factory'; import newModelFactory from './factory';
@ -275,7 +275,7 @@ describe('ItemModel', () => {
test('should respect the hard item size limit', async () => { test('should respect the hard item size limit', async () => {
const { user: user1 } = await createUserAndSession(1); const { user: user1 } = await createUserAndSession(1);
let models = newModelFactory(db(), dbReplica(), config()); let models = newModelFactory(db(), dbSlave(), config());
let result = await models.item().saveFromRawContent(user1, { let result = await models.item().saveFromRawContent(user1, {
body: Buffer.from('1234'), body: Buffer.from('1234'),
@ -285,7 +285,7 @@ describe('ItemModel', () => {
const item = result['test1.txt'].item; const item = result['test1.txt'].item;
config().itemSizeHardLimit = 3; config().itemSizeHardLimit = 3;
models = newModelFactory(db(), dbReplica(), config()); models = newModelFactory(db(), dbSlave(), config());
result = await models.item().saveFromRawContent(user1, { result = await models.item().saveFromRawContent(user1, {
body: Buffer.from('1234'), body: Buffer.from('1234'),
@ -297,7 +297,7 @@ describe('ItemModel', () => {
await expectHttpError(async () => models.item().loadWithContent(item.id), ErrorPayloadTooLarge.httpCode); await expectHttpError(async () => models.item().loadWithContent(item.id), ErrorPayloadTooLarge.httpCode);
config().itemSizeHardLimit = 1000; config().itemSizeHardLimit = 1000;
models = newModelFactory(db(), dbReplica(), config()); models = newModelFactory(db(), dbSlave(), config());
await expectNotThrow(async () => models.item().loadWithContent(item.id)); await expectNotThrow(async () => models.item().loadWithContent(item.id));
}); });
@ -316,18 +316,18 @@ describe('ItemModel', () => {
path: tempDir2, path: tempDir2,
}; };
const fromModels = newModelFactory(db(), dbReplica(), { const fromModels = newModelFactory(db(), dbSlave(), {
...config(), ...config(),
storageDriver: fromStorageConfig, storageDriver: fromStorageConfig,
}); });
const toModels = newModelFactory(db(), dbReplica(), { const toModels = newModelFactory(db(), dbSlave(), {
...config(), ...config(),
storageDriver: toStorageConfig, storageDriver: toStorageConfig,
}); });
const fromDriver = await loadStorageDriver(fromStorageConfig, db(), dbReplica()); const fromDriver = await loadStorageDriver(fromStorageConfig, db(), dbSlave());
const toDriver = await loadStorageDriver(toStorageConfig, db(), dbReplica()); const toDriver = await loadStorageDriver(toStorageConfig, db(), dbSlave());
return { return {
fromStorageConfig, fromStorageConfig,
@ -364,7 +364,7 @@ describe('ItemModel', () => {
await msleep(2); await msleep(2);
const toModels = newModelFactory(db(), dbReplica(), { const toModels = newModelFactory(db(), dbSlave(), {
...config(), ...config(),
storageDriver: toStorageConfig, storageDriver: toStorageConfig,
}); });

View File

@ -75,8 +75,8 @@ export default class ItemModel extends BaseModel<Item> {
private static storageDrivers_: Map<StorageDriverConfig, StorageDriverBase> = new Map(); private static storageDrivers_: Map<StorageDriverConfig, StorageDriverBase> = new Map();
public constructor(db: DbConnection, dbReplica: DbConnection, modelFactory: NewModelFactoryHandler, config: Config) { public constructor(db: DbConnection, dbSlave: DbConnection, modelFactory: NewModelFactoryHandler, config: Config) {
super(db, dbReplica, modelFactory, config); super(db, dbSlave, modelFactory, config);
this.storageDriverConfig_ = config.storageDriver; this.storageDriverConfig_ = config.storageDriver;
this.storageDriverConfigFallback_ = config.storageDriverFallback; this.storageDriverConfigFallback_ = config.storageDriverFallback;
@ -102,7 +102,7 @@ export default class ItemModel extends BaseModel<Item> {
let driver = ItemModel.storageDrivers_.get(config); let driver = ItemModel.storageDrivers_.get(config);
if (!driver) { if (!driver) {
driver = await loadStorageDriver(config, this.db, this.dbReplica); driver = await loadStorageDriver(config, this.db, this.dbSlave);
ItemModel.storageDrivers_.set(config, driver); ItemModel.storageDrivers_.set(config, driver);
} }
@ -331,7 +331,7 @@ export default class ItemModel extends BaseModel<Item> {
let fromDriver: StorageDriverBase = drivers[item.content_storage_id]; let fromDriver: StorageDriverBase = drivers[item.content_storage_id];
if (!fromDriver) { if (!fromDriver) {
fromDriver = await loadStorageDriver(item.content_storage_id, this.db, this.dbReplica); fromDriver = await loadStorageDriver(item.content_storage_id, this.db, this.dbSlave);
drivers[item.content_storage_id] = fromDriver; drivers[item.content_storage_id] = fromDriver;
} }

View File

@ -118,8 +118,8 @@ export default class UserModel extends BaseModel<User> {
private ldapConfig_: LdapConfig[]; private ldapConfig_: LdapConfig[];
public constructor(db: DbConnection, dbReplica: DbConnection, modelFactory: NewModelFactoryHandler, config: Config) { public constructor(db: DbConnection, dbSlave: DbConnection, modelFactory: NewModelFactoryHandler, config: Config) {
super(db, dbReplica, modelFactory, config); super(db, dbSlave, modelFactory, config);
this.ldapConfig_ = config.ldap; this.ldapConfig_ = config.ldap;
} }

View File

@ -83,107 +83,107 @@ export type NewModelFactoryHandler = (db: DbConnection)=> Models;
export class Models { export class Models {
private db_: DbConnection; private db_: DbConnection;
private dbReplica_: DbConnection; private dbSlave_: DbConnection;
private config_: Config; private config_: Config;
public constructor(db: DbConnection, dbReplica_: DbConnection, config: Config) { public constructor(db: DbConnection, dbSlave: DbConnection, config: Config) {
this.db_ = db; this.db_ = db;
this.dbReplica_ = dbReplica_; this.dbSlave_ = dbSlave;
this.config_ = config; this.config_ = config;
this.newModelFactory = this.newModelFactory.bind(this); this.newModelFactory = this.newModelFactory.bind(this);
} }
private newModelFactory(db: DbConnection) { private newModelFactory(db: DbConnection) {
return new Models(db, this.dbReplica_, this.config_); return new Models(db, this.dbSlave_, this.config_);
} }
public item() { public item() {
return new ItemModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new ItemModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public user() { public user() {
return new UserModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new UserModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public email() { public email() {
return new EmailModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new EmailModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public userItem() { public userItem() {
return new UserItemModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new UserItemModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public token() { public token() {
return new TokenModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new TokenModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public itemResource() { public itemResource() {
return new ItemResourceModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new ItemResourceModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public apiClient() { public apiClient() {
return new ApiClientModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new ApiClientModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public session() { public session() {
return new SessionModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new SessionModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public change() { public change() {
return new ChangeModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new ChangeModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public notification() { public notification() {
return new NotificationModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new NotificationModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public share() { public share() {
return new ShareModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new ShareModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public shareUser() { public shareUser() {
return new ShareUserModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new ShareUserModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public keyValue() { public keyValue() {
return new KeyValueModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new KeyValueModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public subscription() { public subscription() {
return new SubscriptionModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new SubscriptionModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public userFlag() { public userFlag() {
return new UserFlagModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new UserFlagModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public event() { public event() {
return new EventModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new EventModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public lock() { public lock() {
return new LockModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new LockModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public storage() { public storage() {
return new StorageModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new StorageModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public userDeletion() { public userDeletion() {
return new UserDeletionModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new UserDeletionModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public backupItem() { public backupItem() {
return new BackupItemModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new BackupItemModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
public taskState() { public taskState() {
return new TaskStateModel(this.db_, this.dbReplica_, this.newModelFactory, this.config_); return new TaskStateModel(this.db_, this.dbSlave_, this.newModelFactory, this.config_);
} }
} }
export default function newModelFactory(db: DbConnection, dbReplica: DbConnection, config: Config): Models { export default function newModelFactory(db: DbConnection, dbSlave: DbConnection, config: Config): Models {
return new Models(db, dbReplica, config); return new Models(db, dbSlave, config);
} }

View File

@ -1,4 +1,4 @@
import { afterAllTests, beforeAllDb, beforeEachDb, db, dbReplica, expectThrow, models } from '../../../utils/testing/testUtils'; import { afterAllTests, beforeAllDb, beforeEachDb, db, dbSlave, expectThrow, models } from '../../../utils/testing/testUtils';
import { StorageDriverType } from '../../../utils/types'; import { StorageDriverType } from '../../../utils/types';
import loadStorageDriver from './loadStorageDriver'; import loadStorageDriver from './loadStorageDriver';
@ -18,13 +18,13 @@ describe('loadStorageDriver', () => {
test('should load a driver and assign an ID to it', async () => { test('should load a driver and assign an ID to it', async () => {
{ {
const newDriver = await loadStorageDriver({ type: StorageDriverType.Memory }, db(), dbReplica()); const newDriver = await loadStorageDriver({ type: StorageDriverType.Memory }, db(), dbSlave());
expect(newDriver.storageId).toBe(1); expect(newDriver.storageId).toBe(1);
expect((await models().storage().count())).toBe(1); expect((await models().storage().count())).toBe(1);
} }
{ {
const newDriver = await loadStorageDriver({ type: StorageDriverType.Filesystem, path: '/just/testing' }, db(), dbReplica()); const newDriver = await loadStorageDriver({ type: StorageDriverType.Filesystem, path: '/just/testing' }, db(), dbSlave());
expect(newDriver.storageId).toBe(2); expect(newDriver.storageId).toBe(2);
expect((await models().storage().count())).toBe(2); expect((await models().storage().count())).toBe(2);
} }

View File

@ -14,7 +14,7 @@ export interface Options {
assignDriverId?: boolean; assignDriverId?: boolean;
} }
export default async function(config: StorageDriverConfig | number, db: DbConnection, dbReplica: DbConnection, options: Options = null): Promise<StorageDriverBase | null> { export default async function(config: StorageDriverConfig | number, db: DbConnection, dbSlave: DbConnection, options: Options = null): Promise<StorageDriverBase | null> {
if (!config) return null; if (!config) return null;
options = { options = {
@ -27,14 +27,14 @@ export default async function(config: StorageDriverConfig | number, db: DbConnec
if (typeof config === 'number') { if (typeof config === 'number') {
storageId = config; storageId = config;
const models = newModelFactory(db, dbReplica, globalConfig()); const models = newModelFactory(db, dbSlave, globalConfig());
const storage = await models.storage().byId(storageId); const storage = await models.storage().byId(storageId);
if (!storage) throw new Error(`No such storage ID: ${storageId}`); if (!storage) throw new Error(`No such storage ID: ${storageId}`);
config = parseStorageDriverConnectionString(storage.connection_string); config = parseStorageDriverConnectionString(storage.connection_string);
} else { } else {
if (options.assignDriverId) { if (options.assignDriverId) {
const models = newModelFactory(db, dbReplica, globalConfig()); const models = newModelFactory(db, dbSlave, globalConfig());
const connectionString = serializeStorageConfig(config); const connectionString = serializeStorageConfig(config);
let storage = await models.storage().byConnectionString(connectionString); let storage = await models.storage().byConnectionString(connectionString);

View File

@ -3,7 +3,7 @@
import config from '../../../config'; import config from '../../../config';
import { Item } from '../../../services/database/types'; import { Item } from '../../../services/database/types';
import { CustomErrorCode } from '../../../utils/errors'; import { CustomErrorCode } from '../../../utils/errors';
import { createUserAndSession, db, dbReplica, makeNoteSerializedBody, models } from '../../../utils/testing/testUtils'; import { createUserAndSession, db, dbSlave, makeNoteSerializedBody, models } from '../../../utils/testing/testUtils';
import { Config, StorageDriverConfig, StorageDriverMode } from '../../../utils/types'; import { Config, StorageDriverConfig, StorageDriverMode } from '../../../utils/types';
import newModelFactory from '../../factory'; import newModelFactory from '../../factory';
import loadStorageDriver from './loadStorageDriver'; import loadStorageDriver from './loadStorageDriver';
@ -15,7 +15,7 @@ const newTestModels = (driverConfig: StorageDriverConfig, driverConfigFallback:
storageDriver: driverConfig, storageDriver: driverConfig,
storageDriverFallback: driverConfigFallback, storageDriverFallback: driverConfigFallback,
}; };
return newModelFactory(db(), dbReplica(), newConfig); return newModelFactory(db(), dbSlave(), newConfig);
}; };
export function shouldWriteToContentAndReadItBack(driverConfig: StorageDriverConfig) { export function shouldWriteToContentAndReadItBack(driverConfig: StorageDriverConfig) {
@ -281,7 +281,7 @@ export function shouldUpdateContentStorageIdAfterSwitchingDriver(oldDriverConfig
export function shouldThrowNotFoundIfNotExist(driverConfig: StorageDriverConfig) { export function shouldThrowNotFoundIfNotExist(driverConfig: StorageDriverConfig) {
test('should throw not found if item does not exist', async () => { test('should throw not found if item does not exist', async () => {
const driver = await loadStorageDriver(driverConfig, db(), dbReplica()); const driver = await loadStorageDriver(driverConfig, db(), dbSlave());
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let error: any = null; let error: any = null;

View File

@ -13,6 +13,19 @@ export interface DropDbOptions {
ignoreIfNotExists: boolean; ignoreIfNotExists: boolean;
} }
const getPostgresToolPath = async (name: string) => {
const candidates = [
'/usr/local/opt/postgresql@16/bin',
];
for (const candidate of candidates) {
const p = `${candidate}/${name}`;
if (await fs.pathExists(p)) return p;
}
return name;
};
export async function createDb(config: DatabaseConfig, options: CreateDbOptions = null) { export async function createDb(config: DatabaseConfig, options: CreateDbOptions = null) {
options = { options = {
dropIfExists: false, dropIfExists: false,
@ -22,7 +35,7 @@ export async function createDb(config: DatabaseConfig, options: CreateDbOptions
if (config.client === 'pg') { if (config.client === 'pg') {
const cmd: string[] = [ const cmd: string[] = [
'createdb', await getPostgresToolPath('createdb'),
'--host', config.host, '--host', config.host,
'--port', config.port.toString(), '--port', config.port.toString(),
'--username', config.user, '--username', config.user,
@ -64,7 +77,7 @@ export async function dropDb(config: DatabaseConfig, options: DropDbOptions = nu
if (config.client === 'pg') { if (config.client === 'pg') {
const cmd: string[] = [ const cmd: string[] = [
'dropdb', await getPostgresToolPath('dropdb'),
'--host', config.host, '--host', config.host,
'--port', config.port.toString(), '--port', config.port.toString(),
'--username', config.user, '--username', config.user,

View File

@ -6,12 +6,12 @@ import { Context } from '../models/items/storage/StorageDriverBase';
import { StorageDriverConfig, StorageDriverType } from './types'; import { StorageDriverConfig, StorageDriverType } from './types';
import { uuidgen } from '@joplin/lib/uuid'; import { uuidgen } from '@joplin/lib/uuid';
export default async function(connection: string | StorageDriverConfig, db: DbConnection, dbReplica: DbConnection, models: Models): Promise<string> { export default async function(connection: string | StorageDriverConfig, db: DbConnection, dbSlave: DbConnection, models: Models): Promise<string> {
const storageConfig = typeof connection === 'string' ? parseStorageConnectionString(connection) : connection; const storageConfig = typeof connection === 'string' ? parseStorageConnectionString(connection) : connection;
if (storageConfig.type === StorageDriverType.Database) return 'Database storage is special and cannot be checked this way. If the connection to the database was successful then the storage driver should work too.'; if (storageConfig.type === StorageDriverType.Database) return 'Database storage is special and cannot be checked this way. If the connection to the database was successful then the storage driver should work too.';
const driver = await loadStorageDriver(storageConfig, db, dbReplica, { assignDriverId: false }); const driver = await loadStorageDriver(storageConfig, db, dbSlave, { assignDriverId: false });
const itemId = `testingconnection${uuidgen(8)}`; const itemId = `testingconnection${uuidgen(8)}`;
const itemContent = Buffer.from(uuidgen(8)); const itemContent = Buffer.from(uuidgen(8));
const context: Context = { models }; const context: Context = { models };

View File

@ -32,6 +32,7 @@ import initLib from '@joplin/lib/initLib';
const packageRootDir = path.dirname(path.dirname(path.dirname(__dirname))); const packageRootDir = path.dirname(path.dirname(path.dirname(__dirname)));
let db_: DbConnection = null; let db_: DbConnection = null;
let dbSlave_: DbConnection = null;
// require('source-map-support').install(); // require('source-map-support').install();
@ -69,12 +70,16 @@ function initGlobalLogger() {
} }
let createdDbPath_: string = null; let createdDbPath_: string = null;
let createdDbSlavePath_: string = null;
export async function beforeAllDb(unitName: string, createDbOptions: CreateDbOptions = null) { export async function beforeAllDb(unitName: string, createDbOptions: CreateDbOptions = null) {
unitName = unitName.replace(/\//g, '_'); unitName = unitName.replace(/\//g, '_');
createdDbPath_ = `${packageRootDir}/db-test-${unitName}.sqlite`; createdDbPath_ = `${packageRootDir}/db-test-${unitName}.sqlite`;
await fs.remove(createdDbPath_); await fs.remove(createdDbPath_);
createdDbSlavePath_ = `${packageRootDir}/db-slave-test-${unitName}.sqlite`;
await fs.remove(createdDbSlavePath_);
const tempDir = `${packageRootDir}/temp/test-${unitName}`; const tempDir = `${packageRootDir}/temp/test-${unitName}`;
await fs.mkdirp(tempDir); await fs.mkdirp(tempDir);
@ -87,16 +92,25 @@ export async function beforeAllDb(unitName: string, createDbOptions: CreateDbOpt
if (process.env.JOPLIN_TESTS_SERVER_DB === 'pg') { if (process.env.JOPLIN_TESTS_SERVER_DB === 'pg') {
await initConfig(Env.Dev, parseEnv({ await initConfig(Env.Dev, parseEnv({
DB_CLIENT: 'pg', DB_CLIENT: 'pg',
DB_USE_SLAVE: '1',
POSTGRES_DATABASE: unitName, POSTGRES_DATABASE: unitName,
POSTGRES_USER: 'joplin', POSTGRES_USER: 'joplin',
POSTGRES_PASSWORD: 'joplin', POSTGRES_PASSWORD: 'joplin',
SLAVE_POSTGRES_DATABASE: unitName,
SLAVE_POSTGRES_USER: 'joplin',
SLAVE_POSTGRES_PASSWORD: 'joplin',
SUPPORT_EMAIL: 'testing@localhost', SUPPORT_EMAIL: 'testing@localhost',
}), { }), {
tempDir: tempDir, tempDir: tempDir,
}); });
} else { } else {
await initConfig(Env.Dev, parseEnv({ await initConfig(Env.Dev, parseEnv({
DB_USE_SLAVE: '1',
SQLITE_DATABASE: createdDbPath_, SQLITE_DATABASE: createdDbPath_,
SLAVE_SQLITE_DATABASE: createdDbSlavePath_,
SUPPORT_EMAIL: 'testing@localhost', SUPPORT_EMAIL: 'testing@localhost',
}), { }), {
tempDir: tempDir, tempDir: tempDir,
@ -108,6 +122,9 @@ export async function beforeAllDb(unitName: string, createDbOptions: CreateDbOpt
await createDb(config().database, { dropIfExists: true, ...createDbOptions }); await createDb(config().database, { dropIfExists: true, ...createDbOptions });
db_ = await connectDb(config().database); db_ = await connectDb(config().database);
await createDb(config().databaseSlave, { dropIfExists: true, ...createDbOptions });
dbSlave_ = await connectDb(config().databaseSlave);
const mustache = new MustacheService(config().viewDir, config().baseUrl); const mustache = new MustacheService(config().viewDir, config().baseUrl);
await mustache.loadPartials(); await mustache.loadPartials();
@ -124,6 +141,11 @@ export async function afterAllTests() {
db_ = null; db_ = null;
} }
if (dbSlave_) {
await disconnectDb(dbSlave_);
dbSlave_ = null;
}
if (tempDir_) { if (tempDir_) {
await fs.remove(tempDir_); await fs.remove(tempDir_);
tempDir_ = null; tempDir_ = null;
@ -257,12 +279,16 @@ export function db() {
return db_; return db_;
} }
export function dbReplica() { export function dbSlave() {
return db_; return dbSlave_;
}
export function dbSlaveSync() {
} }
export function models() { export function models() {
return modelFactory(db(), dbReplica(), config()); return modelFactory(db(), dbSlave(), config());
} }
export function parseHtml(html: string): Document { export function parseHtml(html: string): Document {

View File

@ -165,7 +165,7 @@ export interface Config extends EnvVariables {
accountTypesEnabled: boolean; accountTypesEnabled: boolean;
showErrorStackTraces: boolean; showErrorStackTraces: boolean;
database: DatabaseConfig; database: DatabaseConfig;
databaseReplica: DatabaseConfig; databaseSlave: DatabaseConfig;
mailer: MailerConfig; mailer: MailerConfig;
stripe: StripeConfig; stripe: StripeConfig;
supportEmail: string; supportEmail: string;