Skip to content
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

refactor(server): version logic #9615

Merged
merged 2 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"sanitize-filename": "^1.6.3",
"semver": "^7.6.2",
"sharp": "^0.33.0",
"sirv": "^2.0.4",
"thumbhash": "^0.1.1",
Expand Down
8 changes: 6 additions & 2 deletions server/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Duration } from 'luxon';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { Version } from 'src/utils/version';
import { SemVer } from 'semver';

export const POSTGRES_VERSION_RANGE = '>=14.0.0';
export const VECTORS_VERSION_RANGE = '0.2.x';
export const VECTOR_VERSION_RANGE = '>=0.5 <1';

export const NEXT_RELEASE = 'NEXT_RELEASE';
export const LIFECYCLE_EXTENSION = 'x-immich-lifecycle';
Expand All @@ -11,7 +15,7 @@ export const ADDED_IN_PREFIX = 'This property was added in ';
export const SALT_ROUNDS = 10;

const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
export const serverVersion = Version.fromString(version);
export const serverVersion = new SemVer(version);

export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
Expand Down
2 changes: 1 addition & 1 deletion server/src/database.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ export const databaseConfig: PostgresConnectionOptions = {
*/
export const dataSource = new DataSource({ ...databaseConfig, host: 'localhost' });

export const vectorExt =
export const getVectorExtension = () =>
process.env.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS;
8 changes: 6 additions & 2 deletions server/src/dtos/server-info.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
import { SemVer } from 'semver';
import { SystemConfigThemeDto } from 'src/dtos/system-config.dto';
import { IVersion } from 'src/utils/version';

export class ServerPingResponse {
@ApiResponseProperty({ type: String, example: 'pong' })
Expand All @@ -25,13 +25,17 @@ export class ServerInfoResponseDto {
diskUsagePercentage!: number;
}

export class ServerVersionResponseDto implements IVersion {
export class ServerVersionResponseDto {
@ApiProperty({ type: 'integer' })
major!: number;
@ApiProperty({ type: 'integer' })
minor!: number;
@ApiProperty({ type: 'integer' })
patch!: number;

static fromSemVer(value: SemVer) {
return { major: value.major, minor: value.minor, patch: value.patch };
}
}

export class UsageByUserDto {
Expand Down
15 changes: 6 additions & 9 deletions server/src/interfaces/database.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Version } from 'src/utils/version';

export enum DatabaseExtension {
CUBE = 'cube',
EARTH_DISTANCE = 'earthdistance',
Expand All @@ -23,7 +21,7 @@ export enum DatabaseLock {
GetSystemConfig = 69,
}

export const extName: Record<DatabaseExtension, string> = {
export const EXTENSION_NAMES: Record<DatabaseExtension, string> = {
cube: 'cube',
earthdistance: 'earthdistance',
vector: 'pgvector',
Expand All @@ -37,13 +35,12 @@ export interface VectorUpdateResult {
export const IDatabaseRepository = 'IDatabaseRepository';

export interface IDatabaseRepository {
getExtensionVersion(extensionName: string): Promise<Version | null>;
getAvailableExtensionVersion(extension: DatabaseExtension): Promise<Version | null>;
getPreferredVectorExtension(): VectorExtension;
getPostgresVersion(): Promise<Version>;
getExtensionVersion(extensionName: string): Promise<string | undefined>;
getAvailableExtensionVersion(extension: DatabaseExtension): Promise<string | undefined>;
getPostgresVersion(): Promise<string>;
createExtension(extension: DatabaseExtension): Promise<void>;
updateExtension(extension: DatabaseExtension, version?: Version): Promise<void>;
updateVectorExtension(extension: VectorExtension, version?: Version): Promise<VectorUpdateResult>;
updateExtension(extension: DatabaseExtension, version?: string): Promise<void>;
updateVectorExtension(extension: VectorExtension, version?: string): Promise<VectorUpdateResult>;
reindex(index: VectorIndex): Promise<void>;
shouldReindex(name: VectorIndex): Promise<boolean>;
runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void>;
Expand Down
4 changes: 2 additions & 2 deletions server/src/migrations/1700713871511-UsePgVectors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { vectorExt } from 'src/database.config';
import { getVectorExtension } from 'src/database.config';
import { getCLIPModelInfo } from 'src/utils/misc';
import { MigrationInterface, QueryRunner } from 'typeorm';

Expand All @@ -7,7 +7,7 @@ export class UsePgVectors1700713871511 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`SET search_path TO "$user", public, vectors`);
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${vectorExt}`);
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${getVectorExtension()}`);
const faceDimQuery = await queryRunner.query(`
SELECT CARDINALITY(embedding::real[]) as dimsize
FROM asset_faces
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { vectorExt } from 'src/database.config';
import { getVectorExtension } from 'src/database.config';
import { DatabaseExtension } from 'src/interfaces/database.interface';
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface {
name = 'AddCLIPEmbeddingIndex1700713994428';

public async up(queryRunner: QueryRunner): Promise<void> {
if (vectorExt === DatabaseExtension.VECTORS) {
if (getVectorExtension() === DatabaseExtension.VECTORS) {
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
}
await queryRunner.query(`SET search_path TO "$user", public, vectors`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { vectorExt } from 'src/database.config';
import { getVectorExtension } from 'src/database.config';
import { DatabaseExtension } from 'src/interfaces/database.interface';
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface {
name = 'AddFaceEmbeddingIndex1700714033632';

public async up(queryRunner: QueryRunner): Promise<void> {
if (vectorExt === DatabaseExtension.VECTORS) {
if (getVectorExtension() === DatabaseExtension.VECTORS) {
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
}
await queryRunner.query(`SET search_path TO "$user", public, vectors`);
Expand Down
64 changes: 25 additions & 39 deletions server/src/repositories/database.repository.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { Inject, Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import AsyncLock from 'async-lock';
import { vectorExt } from 'src/database.config';
import semver from 'semver';
import { getVectorExtension } from 'src/database.config';
import {
DatabaseExtension,
DatabaseLock,
EXTENSION_NAMES,
IDatabaseRepository,
VectorExtension,
VectorIndex,
VectorUpdateResult,
extName,
} from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { Instrumentation } from 'src/utils/instrumentation';
import { Version, VersionType } from 'src/utils/version';
import { isValidInteger } from 'src/validation';
import { DataSource, EntityManager, QueryRunner } from 'typeorm';

Expand All @@ -29,68 +29,54 @@ export class DatabaseRepository implements IDatabaseRepository {
this.logger.setContext(DatabaseRepository.name);
}

async getExtensionVersion(extension: DatabaseExtension): Promise<Version | null> {
async getExtensionVersion(extension: DatabaseExtension): Promise<string | undefined> {
const res = await this.dataSource.query(`SELECT extversion FROM pg_extension WHERE extname = $1`, [extension]);
const extVersion = res[0]?.['extversion'];
if (extVersion == null) {
return null;
}

const version = Version.fromString(extVersion);
if (version.isEqual(new Version(0, 1, 1))) {
return new Version(0, 1, 11);
}

return version;
return res[0]?.['extversion'];
}

async getAvailableExtensionVersion(extension: DatabaseExtension): Promise<Version | null> {
async getAvailableExtensionVersion(extension: DatabaseExtension): Promise<string | undefined> {
const res = await this.dataSource.query(
`
SELECT version FROM pg_available_extension_versions
WHERE name = $1 AND installed = false
ORDER BY version DESC`,
[extension],
);
const version = res[0]?.['version'];
return version == null ? null : Version.fromString(version);
return res[0]?.['version'];
}

getPreferredVectorExtension(): VectorExtension {
return vectorExt;
}

async getPostgresVersion(): Promise<Version> {
const res = await this.dataSource.query(`SHOW server_version`);
return Version.fromString(res[0]['server_version']);
async getPostgresVersion(): Promise<string> {
const [{ server_version: version }] = await this.dataSource.query(`SHOW server_version`);
return version;
}

async createExtension(extension: DatabaseExtension): Promise<void> {
await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`);
}

async updateExtension(extension: DatabaseExtension, version?: Version): Promise<void> {
async updateExtension(extension: DatabaseExtension, version?: string): Promise<void> {
await this.dataSource.query(`ALTER EXTENSION ${extension} UPDATE${version ? ` TO '${version}'` : ''}`);
}

async updateVectorExtension(extension: VectorExtension, version?: Version): Promise<VectorUpdateResult> {
const curVersion = await this.getExtensionVersion(extension);
if (!curVersion) {
throw new Error(`${extName[extension]} extension is not installed`);
async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise<VectorUpdateResult> {
const currentVersion = await this.getExtensionVersion(extension);
if (!currentVersion) {
throw new Error(`${EXTENSION_NAMES[extension]} extension is not installed`);
}

const minorOrMajor = version && curVersion.isOlderThan(version) >= VersionType.MINOR;
const isVectors = extension === DatabaseExtension.VECTORS;
let restartRequired = false;
await this.dataSource.manager.transaction(async (manager) => {
await this.setSearchPath(manager);
if (minorOrMajor && isVectors) {
await this.updateVectorsSchema(manager, curVersion);

const isSchemaUpgrade = targetVersion && semver.satisfies(targetVersion, '0.1.1 || 0.1.11');
if (isSchemaUpgrade && isVectors) {
await this.updateVectorsSchema(manager, currentVersion);
}

await manager.query(`ALTER EXTENSION ${extension} UPDATE${version ? ` TO '${version}'` : ''}`);
await manager.query(`ALTER EXTENSION ${extension} UPDATE${targetVersion ? ` TO '${targetVersion}'` : ''}`);

if (!minorOrMajor) {
if (!isSchemaUpgrade) {
return;
}

Expand All @@ -110,7 +96,7 @@ export class DatabaseRepository implements IDatabaseRepository {
try {
await this.dataSource.query(`REINDEX INDEX ${index}`);
} catch (error) {
if (vectorExt === DatabaseExtension.VECTORS) {
if (getVectorExtension() === DatabaseExtension.VECTORS) {
this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`);
const table = index === VectorIndex.CLIP ? 'smart_search' : 'asset_faces';
const dimSize = await this.getDimSize(table);
Expand All @@ -132,7 +118,7 @@ export class DatabaseRepository implements IDatabaseRepository {
}

async shouldReindex(name: VectorIndex): Promise<boolean> {
if (vectorExt !== DatabaseExtension.VECTORS) {
if (getVectorExtension() !== DatabaseExtension.VECTORS) {
return false;
}

Expand Down Expand Up @@ -160,10 +146,10 @@ export class DatabaseRepository implements IDatabaseRepository {
await manager.query(`SET search_path TO "$user", public, vectors`);
}

private async updateVectorsSchema(manager: EntityManager, curVersion: Version): Promise<void> {
private async updateVectorsSchema(manager: EntityManager, currentVersion: string): Promise<void> {
await manager.query('CREATE SCHEMA IF NOT EXISTS vectors');
await manager.query(`UPDATE pg_catalog.pg_extension SET extversion = $1 WHERE extname = $2`, [
curVersion.toString(),
currentVersion,
DatabaseExtension.VECTORS,
]);
await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = true WHERE extname = $1', [
Expand Down
4 changes: 2 additions & 2 deletions server/src/repositories/search.repository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { vectorExt } from 'src/database.config';
import { getVectorExtension } from 'src/database.config';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
Expand Down Expand Up @@ -336,7 +336,7 @@ export class SearchRepository implements ISearchRepository {
}

private getRuntimeConfig(numResults?: number): string {
if (vectorExt === DatabaseExtension.VECTOR) {
if (getVectorExtension() === DatabaseExtension.VECTOR) {
return 'SET LOCAL hnsw.ef_search = 1000;'; // mitigate post-filter recall
}

Expand Down
Loading
Loading