refactor(server): startup checks for vector extension (#11559)

* update update logic

refactor

* update tests

* get version range through repo method, make tests more static

* move "should work" test
This commit is contained in:
Mert
2024-08-05 21:00:25 -04:00
committed by GitHub
parent 9765ccb5a7
commit d5b23373c7
5 changed files with 390 additions and 366 deletions

View File

@@ -1,6 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import semver from 'semver';
import { POSTGRES_VERSION_RANGE, VECTORS_VERSION_RANGE, VECTOR_VERSION_RANGE } from 'src/constants';
import { getVectorExtension } from 'src/database.config';
import { EventHandlerOptions } from 'src/decorators';
import {
@@ -8,6 +7,7 @@ import {
DatabaseLock,
EXTENSION_NAMES,
IDatabaseRepository,
VectorExtension,
VectorIndex,
} from 'src/interfaces/database.interface';
import { OnEvents } from 'src/interfaces/event.interface';
@@ -18,50 +18,46 @@ type UpdateFailedArgs = { name: string; extension: string; availableVersion: str
type RestartRequiredArgs = { name: string; availableVersion: string };
type NightlyVersionArgs = { name: string; extension: string; version: string };
type OutOfRangeArgs = { name: string; extension: string; version: string; range: string };
const EXTENSION_RANGES = {
[DatabaseExtension.VECTOR]: VECTOR_VERSION_RANGE,
[DatabaseExtension.VECTORS]: VECTORS_VERSION_RANGE,
};
type InvalidDowngradeArgs = { name: string; extension: string; installedVersion: string; availableVersion: string };
const messages = {
notInstalled: (name: string) => `Unexpected: The ${name} extension is not installed.`,
notInstalled: (name: string) =>
`The ${name} extension is not available in this Postgres instance.
If using a container image, ensure the image has the extension installed.`,
nightlyVersion: ({ name, extension, version }: NightlyVersionArgs) => `
The ${name} extension version is ${version}, which means it is a nightly release.
The ${name} extension version is ${version}, which means it is a nightly release.
Please run 'DROP EXTENSION IF EXISTS ${extension}' and switch to a release version.
See https://immich.app/docs/guides/database-queries for how to query the database.`,
outOfRange: ({ name, extension, version, range }: OutOfRangeArgs) => `
The ${name} extension version is ${version}, but Immich only supports ${range}.
Please run 'DROP EXTENSION IF EXISTS ${extension}' and switch to a release version.
See https://immich.app/docs/guides/database-queries for how to query the database.`,
outOfRange: ({ name, version, range }: OutOfRangeArgs) =>
`The ${name} extension version is ${version}, but Immich only supports ${range}.
Please change ${name} to a compatible version in the Postgres instance.`,
createFailed: ({ name, extension, otherName }: CreateFailedArgs) =>
`Failed to activate ${name} extension.
Please ensure the Postgres instance has ${name} installed.
If the Postgres instance already has a compatible version installed, Immich may not have the necessary permissions to activate it.
In this case, please run 'ALTER EXTENSION UPDATE ${extension}' manually as a superuser.
See https://immich.app/docs/guides/database-queries for how to query the database.
If the Postgres instance already has ${name} installed, Immich may not have the necessary permissions to activate it.
In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension}' manually as a superuser.
See https://immich.app/docs/guides/database-queries for how to query the database.
Otherwise, please update the version of ${name} in the Postgres instance to a compatible version.`,
createFailed: ({ name, extension, otherName }: CreateFailedArgs) => `
Failed to activate ${name} extension.
Please ensure the Postgres instance has ${name} installed.
Alternatively, if your Postgres instance has ${otherName}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${otherName}'.
Note that switching between the two extensions after a successful startup is not supported.
The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier.
In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup.`,
updateFailed: ({ name, extension, availableVersion }: UpdateFailedArgs) =>
`The ${name} extension can be updated to ${availableVersion}.
Immich attempted to update the extension, but failed to do so.
This may be because Immich does not have the necessary permissions to update the extension.
If the Postgres instance already has ${name} installed, Immich may not have the necessary permissions to activate it.
In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension}' manually as a superuser.
See https://immich.app/docs/guides/database-queries for how to query the database.
Alternatively, if your Postgres instance has ${otherName}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${otherName}'.
Note that switching between the two extensions after a successful startup is not supported.
The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier.
In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup.
`,
updateFailed: ({ name, extension, availableVersion }: UpdateFailedArgs) => `
The ${name} extension can be updated to ${availableVersion}.
Immich attempted to update the extension, but failed to do so.
This may be because Immich does not have the necessary permissions to update the extension.
Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser.
See https://immich.app/docs/guides/database-queries for how to query the database.`,
restartRequired: ({ name, availableVersion }: RestartRequiredArgs) => `
The ${name} extension has been updated to ${availableVersion}.
Please restart the Postgres instance to complete the update.`,
Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser.
See https://immich.app/docs/guides/database-queries for how to query the database.`,
restartRequired: ({ name, availableVersion }: RestartRequiredArgs) =>
`The ${name} extension has been updated to ${availableVersion}.
Please restart the Postgres instance to complete the update.`,
invalidDowngrade: ({ name, installedVersion, availableVersion }: InvalidDowngradeArgs) =>
`The database currently has ${name} ${installedVersion} activated, but the Postgres instance only has ${availableVersion} available.
This most likely means the extension was downgraded.
If ${name} ${installedVersion} is compatible with Immich, please ensure the Postgres instance has this available.`,
};
@Injectable()
@@ -77,74 +73,90 @@ export class DatabaseService implements OnEvents {
async onBootstrapEvent() {
const version = await this.databaseRepository.getPostgresVersion();
const current = semver.coerce(version);
if (!current || !semver.satisfies(current, POSTGRES_VERSION_RANGE)) {
const postgresRange = this.databaseRepository.getPostgresVersionRange();
if (!current || !semver.satisfies(current, postgresRange)) {
throw new Error(
`Invalid PostgreSQL version. Found ${version}, but needed ${POSTGRES_VERSION_RANGE}. Please use a supported version.`,
`Invalid PostgreSQL version. Found ${version}, but needed ${postgresRange}. Please use a supported version.`,
);
}
await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
const extension = getVectorExtension();
const otherExtension =
extension === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS;
const otherName = EXTENSION_NAMES[otherExtension];
const name = EXTENSION_NAMES[extension];
const extensionRange = EXTENSION_RANGES[extension];
const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
try {
await this.databaseRepository.createExtension(extension);
} catch (error) {
this.logger.fatal(messages.createFailed({ name, extension, otherName }));
throw error;
}
const initialVersion = await this.databaseRepository.getExtensionVersion(extension);
const availableVersion = await this.databaseRepository.getAvailableExtensionVersion(extension);
const isAvailable = availableVersion && semver.satisfies(availableVersion, extensionRange);
if (isAvailable && (!initialVersion || semver.gt(availableVersion, initialVersion))) {
try {
this.logger.log(`Updating ${name} extension to ${availableVersion}`);
const { restartRequired } = await this.databaseRepository.updateVectorExtension(extension, availableVersion);
if (restartRequired) {
this.logger.warn(messages.restartRequired({ name, availableVersion }));
}
} catch (error) {
this.logger.warn(messages.updateFailed({ name, extension, availableVersion }));
this.logger.error(error);
}
}
const version = await this.databaseRepository.getExtensionVersion(extension);
if (!version) {
const { availableVersion, installedVersion } = await this.databaseRepository.getExtensionVersion(extension);
if (!availableVersion) {
throw new Error(messages.notInstalled(name));
}
if (semver.eq(version, '0.0.0')) {
throw new Error(messages.nightlyVersion({ name, extension, version }));
if ([availableVersion, installedVersion].some((version) => version && semver.eq(version, '0.0.0'))) {
throw new Error(messages.nightlyVersion({ name, extension, version: '0.0.0' }));
}
if (!semver.satisfies(version, extensionRange)) {
throw new Error(messages.outOfRange({ name, extension, version, range: extensionRange }));
if (!semver.satisfies(availableVersion, extensionRange)) {
throw new Error(messages.outOfRange({ name, extension, version: availableVersion, range: extensionRange }));
}
try {
if (await this.databaseRepository.shouldReindex(VectorIndex.CLIP)) {
await this.databaseRepository.reindex(VectorIndex.CLIP);
}
if (await this.databaseRepository.shouldReindex(VectorIndex.FACE)) {
await this.databaseRepository.reindex(VectorIndex.FACE);
}
} catch (error) {
this.logger.warn(
'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance.',
);
throw error;
if (!installedVersion) {
await this.createExtension(extension);
}
if (installedVersion && semver.gt(availableVersion, installedVersion)) {
await this.updateExtension(extension, availableVersion);
} else if (installedVersion && !semver.satisfies(installedVersion, extensionRange)) {
throw new Error(messages.outOfRange({ name, extension, version: installedVersion, range: extensionRange }));
} else if (installedVersion && semver.lt(availableVersion, installedVersion)) {
throw new Error(messages.invalidDowngrade({ name, extension, availableVersion, installedVersion }));
}
await this.checkReindexing();
if (process.env.DB_SKIP_MIGRATIONS !== 'true') {
await this.databaseRepository.runMigrations();
}
});
}
private async createExtension(extension: DatabaseExtension) {
try {
await this.databaseRepository.createExtension(extension);
} catch (error) {
const otherExtension =
extension === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS;
const name = EXTENSION_NAMES[extension];
this.logger.fatal(messages.createFailed({ name, extension, otherName: EXTENSION_NAMES[otherExtension] }));
throw error;
}
}
private async updateExtension(extension: VectorExtension, availableVersion: string) {
this.logger.log(`Updating ${EXTENSION_NAMES[extension]} extension to ${availableVersion}`);
try {
const { restartRequired } = await this.databaseRepository.updateVectorExtension(extension, availableVersion);
if (restartRequired) {
this.logger.warn(messages.restartRequired({ name: EXTENSION_NAMES[extension], availableVersion }));
}
} catch (error) {
this.logger.warn(messages.updateFailed({ name: EXTENSION_NAMES[extension], extension, availableVersion }));
throw error;
}
}
private async checkReindexing() {
try {
if (await this.databaseRepository.shouldReindex(VectorIndex.CLIP)) {
await this.databaseRepository.reindex(VectorIndex.CLIP);
}
if (await this.databaseRepository.shouldReindex(VectorIndex.FACE)) {
await this.databaseRepository.reindex(VectorIndex.FACE);
}
} catch (error) {
this.logger.warn(
'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance.',
);
throw error;
}
}
}