From 9dbe3b985237836fc9457ec73be3989d2cc440f6 Mon Sep 17 00:00:00 2001 From: Andrii Glushko Date: Wed, 29 Jul 2026 15:03:11 +0200 Subject: [PATCH] enrich logging --- src/PgIpLock.ts | 34 +++++++++++++++++++++++++++++++++- src/PgPubSub.ts | 15 ++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/PgIpLock.ts b/src/PgIpLock.ts index 6c677cc..b63b0ea 100644 --- a/src/PgIpLock.ts +++ b/src/PgIpLock.ts @@ -100,6 +100,7 @@ export class PgIpLock implements AnyLock { private static instances: PgIpLock[] = []; private acquired = false; + private lockedReported = false; private notifyHandler?: (message: Notification) => void; private acquireTimer?: Timeout; @@ -192,13 +193,35 @@ export class PgIpLock implements AnyLock { } this.acquired = true; + + if (this.lockedReported) { + // state change: we were locked out and just took over, which + // means the previous holder's connection is gone + this.lockedReported = false; + this.options.logger.info( + `PgIpLock: acquired '${this.publicChannel}' - the ` + + 'previous holder is gone', + ); + } } catch (err) { // will throw, because insert duplicates existing lock this.acquired = false; const pgErr = err as { code?: string; detail?: string }; - if (!(pgErr.code === 'P0001' && pgErr.detail === 'LOCKED')) { + if (pgErr.code === 'P0001' && pgErr.detail === 'LOCKED') { + // expected under singleListener, but the transition is worth + // one line: "someone else owns it" must be distinguishable + // from "everything is fine" + if (!this.lockedReported) { + this.lockedReported = true; + this.options.logger.info( + `PgIpLock: '${this.publicChannel}' is held by ` + + 'another process, retrying every ' + + `${this.options.acquireInterval}ms`, + ); + } + } else { this.options.logger.error(err); } } @@ -206,6 +229,15 @@ export class PgIpLock implements AnyLock { return this.acquired; } + /** + * Channel name without the internal lock prefix, for log messages + * + * @return {string} + */ + private get publicChannel(): string { + return this.channel.replace(RX_LOCK_CHANNEL, ''); + } + /** * Ensures the lock schema exists, bootstrapping it at most once per * connection: concurrent locks on the same client await a single shared diff --git a/src/PgPubSub.ts b/src/PgPubSub.ts index eed7901..be41fef 100644 --- a/src/PgPubSub.ts +++ b/src/PgPubSub.ts @@ -454,11 +454,19 @@ export class PgPubSub extends EventEmitter { const lock = await this.lock(channel); const acquired = await lock.acquire(); - // ignore else + if (acquired) { await this.pgClient.query(`LISTEN ${ident(channel)}`); this.emit('listen', channel); + + return; } + + // not an error under singleListener - another process owns this + // channel. No log here on purpose: listen() is retried (timer, + // onRelease, reconnect) and would spam. PgIpLock.acquire() reports + // the same fact once per state change, and PgCache summarises the + // resulting coverage as `listening N/M channels`. } /** @@ -746,6 +754,11 @@ export class PgPubSub extends EventEmitter { this.reListenChannels = undefined; + this.logger.info( + `PgPubSub: reconnected after ${this.retry} retry(-ies), ` + + `re-subscribing ${channels.length} channel(s)`, + ); + await Promise.all(channels.map(channel => this.listen(channel))); this.emit('reconnect', this.retry);