diff --git a/src/backend/common/AbstractInitializable.ts b/src/backend/common/AbstractInitializable.ts index 56afa7dc..0a10c034 100644 --- a/src/backend/common/AbstractInitializable.ts +++ b/src/backend/common/AbstractInitializable.ts @@ -5,6 +5,7 @@ import { hasUpstreamError } from "./errors/UpstreamError.js"; import { WebhookPayload } from "./infrastructure/config/health/webhooks.js"; import { AuthCheckError, BuildDataError, ConnectionCheckError, ParseCacheError, PostInitError, StageError, TransformRulesError } from "./errors/MSErrors.js"; import { messageWithCauses, messageWithCausesTruncatedDefault } from "../utils/ErrorUtils.js"; +import { spawn, catchAbortError, isAbortError, rethrowAbortError, delay, forever, AbortError, throwIfAborted } from 'abort-controller-x'; export default abstract class AbstractInitializable { requiresAuth: boolean = false; @@ -12,6 +13,8 @@ export default abstract class AbstractInitializable { authed: boolean = false; authFailure?: boolean; + private initController: AbortController | undefined; + buildOK?: boolean | null; databaseOK?: boolean | null; connectionOK?: boolean | null; @@ -32,45 +35,61 @@ export default abstract class AbstractInitializable { protected abstract getIdentifier(): string; - initialize = async (options: {force?: boolean, notify?: boolean, notifyTitle?: string} = {}) => { + initialize = async (options: {force?: boolean, notify?: boolean, notifyTitle?: string, internalOnly?: boolean} = {}) => { - const {force = false, notify = false, notifyTitle = 'Init Error'} = options; + if(this.initController !== undefined) { + throw new Error(`Already trying to initialize, cannot attempt while an existing initialization attempt is running.`); + } - this.logger.debug('Attempting to initialize...'); - try { - this.initializing = true; - if(this.componentLogger === undefined) { - await this.buildComponentLogger(); - } - await this.buildDatabase(force); - await this.buildInitData(force); - await this.parseCache(force); + this.initController = new AbortController(); + return spawn(this.initController.signal, async (signal, {defer, fork}) => { + + defer(async () => { + this.initializing = false; + this.initController = undefined; + }); + + const {force = false, notify = false, notifyTitle = 'Init Error', internalOnly = false} = options; + + this.logger.debug('Attempting to initialize...'); try { - await this.postCache(); - } catch (e) { - if(e instanceof StageError) { - throw e; - } else { - throw new Error('Error occurred during post-cache hook', {cause: e}); + this.initializing = true; + if(this.componentLogger === undefined) { + await this.buildComponentLogger(); } + await this.buildDatabase(force); + await this.buildInitData(force); + await this.parseCache(force); + try { + await this.postCache(); + } catch (e) { + if(e instanceof StageError) { + throw e; + } else { + throw new Error('Error occurred during post-cache hook', {cause: e}); + } + } + if(internalOnly) { + return true; + } + await this.checkConnection(force); + await this.testAuth(force); + this.logger.info('Fully Initialized!'); + try { + await this.postInitialize(); + } catch (e) { + throw new PostInitError('Error occurred during post-initialization hook', {cause: e}); + } + return true; + } catch(e) { + if(notify) { + await this.notify({title: `${this.getIdentifier()} - ${notifyTitle}`, message: truncateStringToLength(500)(messageWithCausesTruncatedDefault(e)), priority: 'error'}); + } + throw new Error('Initialization failed', {cause: e}); + } finally { + this.initializing = false; } - await this.checkConnection(force); - await this.testAuth(force); - this.logger.info('Fully Initialized!'); - try { - await this.postInitialize(); - } catch (e) { - throw new PostInitError('Error occurred during post-initialization hook', {cause: e}); - } - return true; - } catch(e) { - if(notify) { - await this.notify({title: `${this.getIdentifier()} - ${notifyTitle}`, message: truncateStringToLength(500)(messageWithCausesTruncatedDefault(e)), priority: 'error'}); - } - throw new Error('Initialization failed', {cause: e}); - } finally { - this.initializing = false; - } + }); } protected async buildComponentLogger() { @@ -82,17 +101,6 @@ export default abstract class AbstractInitializable { return; } - tryInitialize = async (options: {force?: boolean, notify?: boolean, notifyTitle?: string} = {}) => { - if(this.initializing) { - throw new Error(`Already trying to initialize, cannot attempt while an existing initialization attempt is running.`) - } - try { - return await this.initialize(options); - } catch (e) { - throw e; - } - } - public async parseCache(force: boolean = false) { if(this.cacheOK) { if(!force) { diff --git a/src/backend/common/transforms/TransformerManager.ts b/src/backend/common/transforms/TransformerManager.ts index 47bf1155..bd3ef4b0 100644 --- a/src/backend/common/transforms/TransformerManager.ts +++ b/src/backend/common/transforms/TransformerManager.ts @@ -96,7 +96,7 @@ export default class TransformerManager { transformer.logger.warn({ label: 'Heartbeat' }, 'Transformer is not ready but will not try to initialize because auth state is not good and cannot be correct unattended.'); } try { - await transformer.tryInitialize({ force: false, notify: true, notifyTitle: 'Could not initialize automatically' }); + await transformer.initialize({ force: false, notify: true, notifyTitle: 'Could not initialize automatically' }); } catch (e) { transformer.logger.error(new Error('Could not initialize source automatically', { cause: e })); } diff --git a/src/backend/index.ts b/src/backend/index.ts index 67a21930..8b2443b0 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -186,7 +186,8 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`) if(nameColl.length > 0) { logger.warn(`Last.FM source and clients have same names [${nameColl.map(x => x.name).join(',')}] -- this may cause issues`); } - const clientInitOptions = {deadDelay: nonEmptyStringOrDefault(process.env.DEBUG_DEAD_DELAY, undefined) !== undefined ? Number.parseInt(process.env.DEBUG_DEAD_DELAY) : undefined}; for(const c of scrobbleClients.clients) { + const clientInitOptions = {deadDelay: nonEmptyStringOrDefault(process.env.DEBUG_DEAD_DELAY, undefined) !== undefined ? Number.parseInt(process.env.DEBUG_DEAD_DELAY) : undefined}; + for(const c of scrobbleClients.clients) { c.initTasks(clientInitOptions); const res = await Promise.race([ sleep(2200), diff --git a/src/backend/scrobblers/AbstractScrobbleClient.ts b/src/backend/scrobblers/AbstractScrobbleClient.ts index 2b403378..17c56568 100644 --- a/src/backend/scrobblers/AbstractScrobbleClient.ts +++ b/src/backend/scrobblers/AbstractScrobbleClient.ts @@ -298,7 +298,7 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i return false; } try { - await this.tryInitialize({force: false, notify: true, notifyTitle: 'Could not initialize automatically'}); + await this.initialize({force: false, notify: true, notifyTitle: 'Could not initialize automatically'}); } catch (e) { this.logger.error(new Error('Could not initialize automatically', {cause: e})); return false; @@ -823,7 +823,7 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i if(!this.isReady() || force) { try { - await this.tryInitialize(options); + await this.initialize(options); } catch (e) { this.logger.error(new Error('Cannot start monitoring because Client is not ready', {cause: e})); if(notify) { diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts index 0a8c80e8..139e66ba 100644 --- a/src/backend/server/auth.ts +++ b/src/backend/server/auth.ts @@ -113,7 +113,7 @@ export const setupAuthRoutes = (app: Express, logger: Logger, sourceMiddle: Expr if(entity instanceof AbstractSource) { entity.poll().catch((e) => logger.error(e)); } else { - entity.tryInitialize() + entity.initialize() .catch((e) => logger.error(e)) .then(() => { entity.initScrobbleMonitoring().catch((e) => logger.error(e)); diff --git a/src/backend/sources/AbstractSource.ts b/src/backend/sources/AbstractSource.ts index 9a273d6e..75b89f1d 100644 --- a/src/backend/sources/AbstractSource.ts +++ b/src/backend/sources/AbstractSource.ts @@ -176,7 +176,7 @@ export default abstract class AbstractSource extends AbstractComponent implement return false; } try { - await this.tryInitialize({force: false, notify: true, notifyTitle: 'Could not initialize automatically'}); + await this.initialize({force: false, notify: true, notifyTitle: 'Could not initialize automatically'}); } catch (e) { this.logger.error(new Error('Could not initialize automatically', {cause: e})); return false; @@ -471,10 +471,9 @@ export default abstract class AbstractSource extends AbstractComponent implement return; } - // TODO refactor to only use tryInitialize if(!this.isReady() || force) { try { - await this.tryInitialize(options); + await this.initialize(options); } catch (e) { this.logger.error(new Error('Cannot start polling because Source is not ready', {cause: e})); if(notify) { diff --git a/src/backend/tests/component/transformers.test.ts b/src/backend/tests/component/transformers.test.ts index b7321829..76a3ea9b 100644 --- a/src/backend/tests/component/transformers.test.ts +++ b/src/backend/tests/component/transformers.test.ts @@ -419,7 +419,7 @@ describe('Play Transforms', function () { it('Uses artist parsing functions', async function() { const t = new NativeTransformer({name: 'test', type: 'native'}, {logger: loggerTest, cache: memorycache()}); - await t.tryInitialize(); + await t.initialize(); const [str, primaries, secondaries] = generateArtistsStr({primary: {max: 3, ambiguousJoinedNames: true, trailingAmpersand: true, finalJoiner: false}}); const play = generatePlay({artists: artistNamesToCredits([str])}); @@ -434,7 +434,7 @@ describe('Play Transforms', function () { const t = new NativeTransformer({name: 'test', type: 'native', defaults: {artistsIgnore: [str]}}, {logger: loggerTest, cache: memorycache()}); - await t.tryInitialize(); + await t.initialize(); const play = generatePlay({artists: artistNamesToCredits([str]), track: 'My Test'}); @@ -455,7 +455,7 @@ describe('Play Transforms', function () { const t = new NativeTransformer({name: 'test', type: 'native', defaults: {delimitersExtra: ['•']}}, {logger: loggerTest, cache: memorycache()}); - await t.tryInitialize(); + await t.initialize(); const play = generatePlay({artists: artistNamesToCredits([str]), track: 'My Test'}); diff --git a/src/backend/tests/musicbrainz/musicbrainz.test.ts b/src/backend/tests/musicbrainz/musicbrainz.test.ts index a07f1ac6..241af999 100644 --- a/src/backend/tests/musicbrainz/musicbrainz.test.ts +++ b/src/backend/tests/musicbrainz/musicbrainz.test.ts @@ -70,7 +70,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const res = await mbTransformer.getTransformerData(play, { type: "musicbrainz", @@ -101,7 +101,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } }; - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const res = await mbTransformer.getTransformerData(play, { type: "musicbrainz", @@ -131,7 +131,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const res = await mbTransformer.getTransformerData(play, { type: "musicbrainz", @@ -160,7 +160,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const res = await mbTransformer.getTransformerData(play, { type: "musicbrainz", @@ -191,7 +191,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const stageConfig: MusicbrainzTransformerDataStage = { type: "musicbrainz", @@ -225,7 +225,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const stageConfig: MusicbrainzTransformerDataStage = { type: "musicbrainz", @@ -254,7 +254,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const res = await mbTransformer.getTransformerData(play, { type: "musicbrainz", @@ -278,7 +278,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const res = await mbTransformer.getTransformerData(play, { type: "musicbrainz", @@ -304,7 +304,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const res = await mbTransformer.getTransformerData(play, { type: "musicbrainz", @@ -329,7 +329,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const stageConfig: MusicbrainzTransformerDataStage = { type: "musicbrainz", @@ -359,7 +359,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await mbTransformer.tryInitialize(); + await mbTransformer.initialize(); const res = await mbTransformer.getTransformerData(play, { type: "musicbrainz", @@ -403,7 +403,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await multiMb.tryInitialize(); + await multiMb.initialize(); const res = await multiMb.getTransformerData(play, { type: "musicbrainz", @@ -437,7 +437,7 @@ describe('Musicbrainz API', function () { lifecycle: defaultLifecycle() } } - await multiMb.tryInitialize(); + await multiMb.initialize(); const res = await multiMb.getTransformerData(play, { type: "musicbrainz", diff --git a/src/backend/tests/scrobbler/scrobblers.test.ts b/src/backend/tests/scrobbler/scrobblers.test.ts index d911be6c..8bcd6b3b 100644 --- a/src/backend/tests/scrobbler/scrobblers.test.ts +++ b/src/backend/tests/scrobbler/scrobblers.test.ts @@ -52,7 +52,7 @@ const generateTestScrobbler = async () => { confidenceBreakdown: true } }; - await testScrobbler.tryInitialize(); + await testScrobbler.initialize(); return testScrobbler; } @@ -523,7 +523,7 @@ describe('Upstream Scrobbles', function() { const existingPlays = normalizePlays(generatePlays(3), {initialDate: dayjs().subtract(1, 'hour')}); await using scrobbler = await generateTestScrobbler(); scrobbler.testRecentScrobbles = []; - await scrobbler.tryInitialize(); + await scrobbler.initialize(); scrobbler.testRecentScrobbles = existingPlays; const sp = spy(scrobbler, 'getScrobblesForTimeRange'); @@ -542,7 +542,7 @@ describe('Upstream Scrobbles', function() { const existingPlays = normalizePlays(generatePlays(3), {initialDate: dayjs().subtract(1, 'hour')}); await using scrobbler = await generateTestScrobbler(); scrobbler.testRecentScrobbles = []; - await scrobbler.tryInitialize(); + await scrobbler.initialize(); scrobbler.testRecentScrobbles = existingPlays; const sp = spy(scrobbler, 'getScrobblesForTimeRange'); @@ -562,7 +562,7 @@ describe('Upstream Scrobbles', function() { const existingPlays = normalizePlays(generatePlays(3), {initialDate: dayjs().subtract(1, 'hour')}); await using scrobbler = await generateTestScrobbler(); scrobbler.testRecentScrobbles = []; - await scrobbler.tryInitialize(); + await scrobbler.initialize(); scrobbler.testRecentScrobbles = existingPlays; const sp = spy(scrobbler, 'getScrobblesForTimeRange'); @@ -583,7 +583,7 @@ describe('Upstream Scrobbles', function() { const existingPlays = normalizePlays(generatePlays(3), {initialDate: dayjs().subtract(1, 'hour')}); await using scrobbler = await generateTestScrobbler(); scrobbler.testRecentScrobbles = []; - await scrobbler.tryInitialize(); + await scrobbler.initialize(); scrobbler.testRecentScrobbles = existingPlays; const sp = spy(scrobbler, 'getScrobblesForTimeRange'); diff --git a/src/backend/tests/source/source.test.ts b/src/backend/tests/source/source.test.ts index 48a2f4f3..8b04678c 100644 --- a/src/backend/tests/source/source.test.ts +++ b/src/backend/tests/source/source.test.ts @@ -27,12 +27,12 @@ chai.use(asPromised); const emitter = new EventEmitter(); const generateSource = async () => { const source = new TestSource('spotify', 'test-basic', {}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); - await source.tryInitialize(); + await source.initialize(); return source; } const generateMemorySource = async (config: SourceConfig = {}) => { const s = new TestMemorySource('spotify', 'test-memory', config, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); - await s.tryInitialize(); + await s.initialize(); // s.buildTransformRules(); s.scheduler.stop(); return s; @@ -40,7 +40,7 @@ const generateMemorySource = async (config: SourceConfig = {}) => { const generateMemoryPositionalSource = async (config: SourceConfig = {}) => { const s = new TestMemoryPositionalSource('spotify', 'test-positional', config, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); - await s.tryInitialize(); + await s.initialize(); //s.buildTransformRules(); s.scheduler.stop(); return s; @@ -437,7 +437,7 @@ class DeezerTestSource extends DeezerInternalSource { const generateDeezerSource = async (options: DeezerInternalSourceOptions = {}) => { const source = new DeezerTestSource('test', {data: {arl: 'test'}, options}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); - await source.tryInitialize(); + await source.initialize(); return source; } const firstPlayDate = dayjs().subtract(1, 'hour');