[patch] Cache invalidation for I/O Workers
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253843psql -h localhost -U postgresBuilt from patchset v6 (message #6), October 22, 2026 at 03:52 PM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t253843_6 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t253843_6 && git checkout t253843_6Patchset v6 (message #6) is on t253843_6
This solves BUG #19622, from pgsql-bugs [1]
With io_method=worker I/O worker processes retain open file descriptors on
relation files that have been dropped.
When dropping a relation, an invalidation message is posted, backends handle it
this patch implements the same pattern in I/O worker, and prevents holding
invalid relations.
That said,
This issue doesn't have observable consequences to the user, and is
currently bound
by `max_files_per_process` (default 1000), after which it forces
closing the least
recently used files, if that parameter does its job, we wouldn't have a crash.
In the theoretical scenario of a relation number being reused it could
attempt to read
from a stale file.
Hi Alexandre,
Thanks for looking into this issue. It seems the patch is missing from the
attachment.
Also, would you mind creating a new entry in commifeast?
Thanks,
Shihao
The forgotten patch
Show quoted text
On Fri, Sep 18, 2026 at 5:59 PM shihao zhong <zhong950419@gmail.com> wrote:
Hi Alexandre,
Thanks for looking into this issue. It seems the patch is missing from the attachment.
Also, would you mind creating a new entry in commifeast?
Thanks,
Shihao
On Fri, Sep 18, 2026 at 5:59 PM shihao zhong <zhong950419@gmail.com> wrote:
Hi Alexandre,
Thanks for looking into this issue. It seems the patch is missing from the attachment.
Also, would you mind creating a new entry in commifeast?
Hi Alexandre,
Thanks for the patch. I read v1, here are my suggestions.
+ if (msg->id == SHAREDINVALSMGR_ID)
+ {
+ RelFileLocatorBackend rlocator;
+ rlocator.locator = msg->sm.rlocator;
+ rlocator.backend = (msg->sm.backend_hi << 16) | (int) msg->sm.backend_lo;
+ smgrreleaserellocator(rlocator);
+ }
Since 21d9c3ee4ef, smgrreleaserellocator() only closes the fds. The
SMgrRelation
entry stays in the hash table. Backends free those in AtEOXact_SMgr(), but
an
IO worker never runs a transaction, so entries for dropped relations
pile up. The fd leak turns into a memory leak. We had runs into production
issue because of that.
bgwriter and checkpointer have the same problem. They call
smgrdestroyall() once per checkpoint cycle, see the comment in
bgwriter.c. Could the IO worker just do the same every so often? That
is option 1 in the bug report. It needs no sinval slot for up to 32
workers, and it is small enough to backpatch to 18, where
io_method=worker is the default.
The patch also needs a pgindent run.
Thanks,
Shihao
On Sun, Sep 20, 2026 at 10:17 PM shihao zhong <zhong950419@gmail.com> wrote:
Hi Alexandre,
Thanks for the patch. I read v1, here are my suggestions.
+ if (msg->id == SHAREDINVALSMGR_ID) + { + RelFileLocatorBackend rlocator; + rlocator.locator = msg->sm.rlocator; + rlocator.backend = (msg->sm.backend_hi << 16) | (int) msg->sm.backend_lo; + smgrreleaserellocator(rlocator); + }Since 21d9c3ee4ef, smgrreleaserellocator() only closes the fds. The
SMgrRelation
entry stays in the hash table. Backends free those in AtEOXact_SMgr(), but
an
IO worker never runs a transaction, so entries for dropped relations
pile up. The fd leak turns into a memory leak. We had runs into production
issue because of that.
I see now, what I understood is that smgrrelease() only closes,
smgrdestroy() closes
and releases memory. Updated the patch and the function comments trying to
make this
distinction more evident.
bgwriter and checkpointer have the same problem. They call
smgrdestroyall() once per checkpoint cycle, see the comment in
bgwriter.c. Could the IO worker just do the same every so often?
I saw a proposal for what you described [1], but it doesn't seem ideal as
It keeps stale handles for much longer than necessary, and closes files
unnecessarily after each checkpoint.
That
is option 1 in the bug report. It needs no sinval slot for up to 32
workers,
Are the sinval entries concerning? If so, I think it could have something
way simpler,
since we have a reset for when the buffer overflows, all we would need to
have is
a circular queue of messages and each worker saving its position. If
pending messages
is above the circular queue capacity it is a reset. This way the
invalidation list doesn't
even need to be aware of who is reading it.
and it is small enough to backpatch to 18, where
io_method=worker is the default.
Indeed that is smaller, maybe backpatch with [1] and using this going
forward.
The patch also needs a pgindent run.
Applied this time.
While checking SharedInvalBackendInit I noticed this
sinval.c
287 /*
288 * This can run in parallel with read operations, but not with write
289 * operations, since SIInsertDataEntries relies on the pgprocnos array to
290 * set hasMessages appropriately.
291 */
292 LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);
It sounds like we could use a LW_SHARED lock there.
1.
/messages/by-id/CAN55FZ2BesKUnajdgpw1fPSe3S6_CHOugryaUEtD7vdP=dRKEQ@mail.gmail.com
Regards,
Alexandre
Show quoted text
Hi Alexandre,
v2 looks right to me, the entries are freed now.
An idle worker sleeps in WaitLatch, so it only sees the messages after
a catchup signal, that is 2048 queued messages. On a quiet system the
stale fds stay, like in the 51 minutes case from the bug report.
Nazir's patch covers that case, yours covers the busy worker. Maybe the
two threads should be merged, CCing Nazir.
LW_SHARED does not work there. SharedInvalBackendInit does
pgprocnos[numProcs++] = MyProcNumber, two backends starting at the same
time would both hold the shared lock and race on numProcs.
Small things: a catchup wakeup is counted in hist_wakeups, the comment
rewrites in smgr.c are unrelated, and shmem.h sorts before sinvaladt.h.
Thanks,
Shihao