import env from '#start/env' import logger from '@adonisjs/core/services/logger' import coordinatorConfig from '#config/coordinator' import { XoverClient } from '#lib/nntp/index' /** * The coordinator's header connections (REBUILD.md, decision e: workers * have no NNTP account). At most NNTP_CONNECTIONS, reused; a connection * that failed is closed and replaced on the next use. */ class NntpPool { private idle: XoverClient[] = [] private busy = new Set() private open = 0 private waiting: ((client: XoverClient) => void)[] = [] get size() { return coordinatorConfig.nntpConnections } async use(work: (client: XoverClient) => Promise): Promise { const client = await this.acquire() this.busy.add(client) let broken = false try { return await work(client) } catch (error) { broken = true throw error } finally { this.busy.delete(client) this.release(client, broken) } } private async acquire(): Promise { const ready = this.idle.pop() if (ready) return ready if (this.open < this.size) { this.open++ try { return await XoverClient.connect({ host: env.get('NNTP_HOST')!, port: env.get('NNTP_PORT', 563), user: env.get('NNTP_USER'), password: env.get('NNTP_PASSWORD'), secure: env.get('NNTP_SECURE', true), compress: true, timeoutMs: coordinatorConfig.nntpTimeoutMs, }) } catch (error) { this.open-- throw error } } return new Promise((resolve) => this.waiting.push(resolve)) } private release(client: XoverClient, broken: boolean) { if (broken) { this.open-- try { client.close() } catch (error: any) { logger.debug(`Closing a failed NNTP connection: ${error.message}`) } // Someone waiting gets a fresh connection. const next = this.waiting.shift() if (next) this.acquire().then(next, () => this.waiting.unshift(next)) return } const next = this.waiting.shift() if (next) next(client) else this.idle.push(client) } /** Closes every connection, also those in use: their commands fail. */ private plain: Promise | null = null private plainQueue: Promise = Promise.resolve() /** * One connection without XFEATURE COMPRESS GZIP, opened when first needed, * one command at a time: for chunks whose compressed reply the server * cuts short (Leases.corrupt). Its replies are deflated here. */ async usePlain(work: (client: XoverClient) => Promise): Promise { const run = this.plainQueue.then(async () => { this.plain ??= XoverClient.connect({ host: env.get('NNTP_HOST')!, port: env.get('NNTP_PORT', 563), user: env.get('NNTP_USER'), password: env.get('NNTP_PASSWORD'), secure: env.get('NNTP_SECURE', true), compress: false, timeoutMs: coordinatorConfig.nntpTimeoutMs, }) const client = await this.plain try { return await work(client) } catch (error) { this.plain = null client.close() throw error } }) this.plainQueue = run.catch(() => {}) return run } async close() { void this.plain?.then((c) => c.close()).catch(() => {}) this.plain = null for (const client of [...this.idle.splice(0), ...this.busy]) client.close() this.busy.clear() this.open = 0 } } export default new NntpPool()