mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	perf: cache getConfig (#9377)
				
					
				
			* cache `getConfig` * critical fix Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --------- Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									35ef82ab6b
								
							
						
					
					
						commit
						f3fbb9b588
					
				@ -1,5 +1,6 @@
 | 
				
			|||||||
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
 | 
					import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
 | 
				
			||||||
import { CronExpression } from '@nestjs/schedule';
 | 
					import { CronExpression } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					import AsyncLock from 'async-lock';
 | 
				
			||||||
import { plainToInstance } from 'class-transformer';
 | 
					import { plainToInstance } from 'class-transformer';
 | 
				
			||||||
import { validate } from 'class-validator';
 | 
					import { validate } from 'class-validator';
 | 
				
			||||||
import { load as loadYaml } from 'js-yaml';
 | 
					import { load as loadYaml } from 'js-yaml';
 | 
				
			||||||
@ -21,6 +22,7 @@ import {
 | 
				
			|||||||
  TranscodePolicy,
 | 
					  TranscodePolicy,
 | 
				
			||||||
  VideoCodec,
 | 
					  VideoCodec,
 | 
				
			||||||
} from 'src/entities/system-config.entity';
 | 
					} from 'src/entities/system-config.entity';
 | 
				
			||||||
 | 
					import { DatabaseLock } from 'src/interfaces/database.interface';
 | 
				
			||||||
import { QueueName } from 'src/interfaces/job.interface';
 | 
					import { QueueName } from 'src/interfaces/job.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
 | 
					import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
 | 
				
			||||||
@ -186,7 +188,9 @@ let instance: SystemConfigCore | null;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SystemConfigCore {
 | 
					export class SystemConfigCore {
 | 
				
			||||||
  private configCache: SystemConfigEntity<SystemConfigValue>[] | null = null;
 | 
					  private readonly asyncLock = new AsyncLock();
 | 
				
			||||||
 | 
					  private config: SystemConfig | null = null;
 | 
				
			||||||
 | 
					  private lastUpdated: number | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public config$ = new Subject<SystemConfig>();
 | 
					  public config$ = new Subject<SystemConfig>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -268,32 +272,17 @@ export class SystemConfigCore {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async getConfig(force = false): Promise<SystemConfig> {
 | 
					  public async getConfig(force = false): Promise<SystemConfig> {
 | 
				
			||||||
    const configFilePath = process.env.IMMICH_CONFIG_FILE;
 | 
					    if (force || !this.config) {
 | 
				
			||||||
    const config = _.cloneDeep(defaults);
 | 
					      const lastUpdated = this.lastUpdated;
 | 
				
			||||||
    const overrides = configFilePath ? await this.loadFromFile(configFilePath, force) : await this.repository.load();
 | 
					      await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
 | 
				
			||||||
 | 
					        if (lastUpdated === this.lastUpdated) {
 | 
				
			||||||
    for (const { key, value } of overrides) {
 | 
					          this.config = await this.buildConfig();
 | 
				
			||||||
      // set via dot notation
 | 
					          this.lastUpdated = Date.now();
 | 
				
			||||||
      _.set(config, key, value);
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const errors = await validate(plainToInstance(SystemConfigDto, config));
 | 
					    return this.config!;
 | 
				
			||||||
    if (errors.length > 0) {
 | 
					 | 
				
			||||||
      this.logger.error('Validation error', errors);
 | 
					 | 
				
			||||||
      if (configFilePath) {
 | 
					 | 
				
			||||||
        throw new Error(`Invalid value(s) in file: ${errors}`);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
 | 
					 | 
				
			||||||
      config.ffmpeg.acceptedVideoCodecs.unshift(config.ffmpeg.targetVideoCodec);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
 | 
					 | 
				
			||||||
      config.ffmpeg.acceptedAudioCodecs.unshift(config.ffmpeg.targetAudioCodec);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return config;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
 | 
					  public async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
 | 
				
			||||||
@ -345,8 +334,38 @@ export class SystemConfigCore {
 | 
				
			|||||||
    this.config$.next(newConfig);
 | 
					    this.config$.next(newConfig);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async loadFromFile(filepath: string, force = false) {
 | 
					  private async buildConfig() {
 | 
				
			||||||
    if (force || !this.configCache) {
 | 
					    const config = _.cloneDeep(defaults);
 | 
				
			||||||
 | 
					    const overrides = process.env.IMMICH_CONFIG_FILE
 | 
				
			||||||
 | 
					      ? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE)
 | 
				
			||||||
 | 
					      : await this.repository.load();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const { key, value } of overrides) {
 | 
				
			||||||
 | 
					      // set via dot notation
 | 
				
			||||||
 | 
					      _.set(config, key, value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const errors = await validate(plainToInstance(SystemConfigDto, config));
 | 
				
			||||||
 | 
					    if (errors.length > 0) {
 | 
				
			||||||
 | 
					      if (process.env.IMMICH_CONFIG_FILE) {
 | 
				
			||||||
 | 
					        throw new Error(`Invalid value(s) in file: ${errors}`);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.logger.error('Validation error', errors);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
 | 
				
			||||||
 | 
					      config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
 | 
				
			||||||
 | 
					      config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return config;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async loadFromFile(filepath: string) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const file = await this.repository.readFile(filepath);
 | 
					      const file = await this.repository.readFile(filepath);
 | 
				
			||||||
      const config = loadYaml(file.toString()) as any;
 | 
					      const config = loadYaml(file.toString()) as any;
 | 
				
			||||||
@ -364,16 +383,13 @@ export class SystemConfigCore {
 | 
				
			|||||||
        this.logger.warn(`Unknown keys found: ${JSON.stringify(config, null, 2)}`);
 | 
					        this.logger.warn(`Unknown keys found: ${JSON.stringify(config, null, 2)}`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.configCache = overrides;
 | 
					      return overrides;
 | 
				
			||||||
    } catch (error: Error | any) {
 | 
					    } catch (error: Error | any) {
 | 
				
			||||||
      this.logger.error(`Unable to load configuration file: ${filepath}`);
 | 
					      this.logger.error(`Unable to load configuration file: ${filepath}`);
 | 
				
			||||||
      throw error;
 | 
					      throw error;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return this.configCache;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private unsetDeep(object: object, key: string) {
 | 
					  private unsetDeep(object: object, key: string) {
 | 
				
			||||||
    _.unset(object, key);
 | 
					    _.unset(object, key);
 | 
				
			||||||
    const path = key.split('.');
 | 
					    const path = key.split('.');
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,7 @@ export enum DatabaseLock {
 | 
				
			|||||||
  StorageTemplateMigration = 420,
 | 
					  StorageTemplateMigration = 420,
 | 
				
			||||||
  CLIPDimSize = 512,
 | 
					  CLIPDimSize = 512,
 | 
				
			||||||
  LibraryWatch = 1337,
 | 
					  LibraryWatch = 1337,
 | 
				
			||||||
 | 
					  GetSystemConfig = 69,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const extName: Record<DatabaseExtension, string> = {
 | 
					export const extName: Record<DatabaseExtension, string> = {
 | 
				
			||||||
 | 
				
			|||||||
@ -99,19 +99,22 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('init', () => {
 | 
					  describe('init', () => {
 | 
				
			||||||
    beforeEach(async () => {
 | 
					    it('should pause and resume queue during init', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await sut.init();
 | 
					      await sut.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(jobMock.pause).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					      expect(metadataMock.init).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					      expect(jobMock.resume).toHaveBeenCalledTimes(1);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should return if reverse geocoding is disabled', async () => {
 | 
					    it('should return if reverse geocoding is disabled', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: false }]);
 | 
					      configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: false }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.init();
 | 
					      await sut.init();
 | 
				
			||||||
      expect(jobMock.pause).toHaveBeenCalledTimes(1);
 | 
					
 | 
				
			||||||
      expect(metadataMock.init).toHaveBeenCalledTimes(1);
 | 
					      expect(jobMock.pause).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(jobMock.resume).toHaveBeenCalledTimes(1);
 | 
					      expect(metadataMock.init).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.resume).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user