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. */ async close() { for (const client of [...this.idle.splice(0), ...this.busy]) client.close() this.busy.clear() this.open = 0 } } export default new NntpPool()