public inbox for [email protected]help / color / mirror / Atom feed
[PATCH v1 2/2] WIP: buffer alloc specialized for relation extension 23+ messages / 2 participants [nested] [flat]
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension @ 2020-12-31 12:20 Luc Vlaming <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Luc Vlaming @ 2020-12-31 12:20 UTC (permalink / raw) --- src/backend/storage/buffer/bufmgr.c | 280 +++++++++++++++++++++++++++- src/include/storage/buf_internals.h | 2 +- 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d1ce88fee5..14f0923bb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -474,6 +474,10 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr, BlockNumber blockNum, BufferAccessStrategy strategy, bool *foundPtr); +static BufferDesc *BufferAllocExtend(SMgrRelation smgr, + char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln); static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); @@ -996,7 +1000,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) BlockNumber firstBlock, blockNum; Block bufBlock; - bool found; + LWLock* lastPartitionLock = NULL; /* Open it at the smgr level if not already done */ RelationOpenSmgr(reln); @@ -1007,7 +1011,7 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) LockRelationForExtension(reln, ExclusiveLock); firstBlock = smgrnblocks(smgr, MAIN_FORKNUM); - smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, false, BULK_INSERT_BATCH_SIZE); + smgrextend_count(smgr, MAIN_FORKNUM, firstBlock, bistate->empty_buffer, true, BULK_INSERT_BATCH_SIZE); UnlockRelationForExtension(reln, ExclusiveLock); for (int i=0; i<BULK_INSERT_BATCH_SIZE; ++i) @@ -1017,12 +1021,10 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) pgstat_count_buffer_read(reln); /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlargeBuffers(CurrentResourceOwner); - bufHdr = BufferAlloc(smgr, relpersistence, MAIN_FORKNUM, blockNum, - bistate->strategy, &found); + bufHdr = BufferAllocExtend(smgr, relpersistence, MAIN_FORKNUM, blockNum, + bistate->strategy, &lastPartitionLock); pgBufferUsage.shared_blks_written++; - Assert(!found); - bufBlock = BufHdrGetBlock(bufHdr); /* new buffers are zero-filled */ @@ -1033,6 +1035,9 @@ ReadBufferExtendBulk(Relation reln, struct BulkInsertStateData* bistate) bistate->local_buffers[i] = BufferDescriptorGetBuffer(bufHdr); } + Assert(lastPartitionLock); + LWLockRelease(lastPartitionLock); + bistate->local_buffers_idx = 0; VacuumPageMiss += BULK_INSERT_BATCH_SIZE; @@ -1408,6 +1413,269 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, return buf; } +static BufferDesc * +BufferAllocExtend(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, + BlockNumber blockNum, BufferAccessStrategy strategy, + LWLock** lastPartitionLock) +{ + BufferTag newTag; /* identity of requested block */ + uint32 newHash; /* hash value for newTag */ + LWLock *newPartitionLock; /* buffer partition lock for it */ + BufferTag oldTag; /* previous identity of selected buffer */ + uint32 oldHash; /* hash value for oldTag */ + LWLock *oldPartitionLock; /* buffer partition lock for it */ + uint32 oldFlags; + int buf_id; + BufferDesc *buf; + uint32 buf_state; + + Assert(lastPartitionLock); + + /* create a tag so we can lookup the buffer */ + INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); + + /* determine its hash code and partition lock ID */ + newHash = BufTableHashCode(&newTag); + newPartitionLock = BufMappingPartitionLock(newHash); + + /* Loop here in case we have to try another victim buffer */ + for (;;) + { + /* + * Ensure, while the spinlock's not yet held, that there's a free + * refcount entry. + */ + ReservePrivateRefCountEntry(); + + /* + * Select a victim buffer. The buffer is returned with its header + * spinlock still held! + */ + buf = StrategyGetBuffer(strategy, &buf_state); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); + + /* Must copy buffer flags while we still hold the spinlock */ + oldFlags = buf_state & BUF_FLAG_MASK; + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(buf); + + /* + * If the buffer was dirty, try to write it out. There is a race + * condition here, in that someone might dirty it after we released it + * above, or even while we are writing it out (since our share-lock + * won't prevent hint-bit updates). We will recheck the dirty bit + * after re-locking the buffer header. + */ + if (oldFlags & BM_DIRTY) + { + /* + * We need a share-lock on the buffer contents to write it out + * (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even + * though the buffer was not pinned (and therefore surely not + * locked) when StrategyGetBuffer returned it, someone else could + * have pinned and exclusive-locked it by the time we get here. If + * we try to get the lock unconditionally, we'd block waiting for + * them; if they later block waiting for us, deadlock ensues. + * (This has been observed to happen when two backends are both + * trying to split btree index pages, and the second one just + * happens to be trying to split the page the first one got from + * StrategyGetBuffer.) + */ + if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), + LW_SHARED)) + { + /* + * If using a nondefault strategy, and writing the buffer + * would require a WAL flush, let the strategy decide whether + * to go ahead and write/reuse the buffer or to choose another + * victim. We need lock to inspect the page LSN, so this + * can't be done inside StrategyGetBuffer. + */ + if (strategy != NULL) + { + XLogRecPtr lsn; + + /* Read the LSN while holding buffer header lock */ + buf_state = LockBufHdr(buf); + lsn = BufferGetLSN(buf); + UnlockBufHdr(buf, buf_state); + + if (XLogNeedsFlush(lsn) && + StrategyRejectBuffer(strategy, buf)) + { + /* Drop lock/pin and loop around for another buffer */ + LWLockRelease(BufferDescriptorGetContentLock(buf)); + UnpinBuffer(buf, true); + continue; + } + } + + /* OK, do the I/O */ + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + + FlushBuffer(buf, NULL); + LWLockRelease(BufferDescriptorGetContentLock(buf)); + + ScheduleBufferTagForWriteback(&BackendWritebackContext, + &buf->tag); + + TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum, + smgr->smgr_rnode.node.spcNode, + smgr->smgr_rnode.node.dbNode, + smgr->smgr_rnode.node.relNode); + } + else + { + /* + * Someone else has locked the buffer, so give it up and loop + * back to get another one. + */ + UnpinBuffer(buf, true); + continue; + } + } + + /* + * To change the association of a valid buffer, we'll need to have + * exclusive lock on both the old and new mapping partitions. + */ + if (oldFlags & BM_TAG_VALID) + { + /* + * Need to compute the old tag's hashcode and partition lock ID. + * XXX is it worth storing the hashcode in BufferDesc so we need + * not recompute it here? Probably not. + */ + oldTag = buf->tag; + oldHash = BufTableHashCode(&oldTag); + oldPartitionLock = BufMappingPartitionLock(oldHash); + + if (*lastPartitionLock && + (*lastPartitionLock != oldPartitionLock || + *lastPartitionLock != newPartitionLock)) + { + LWLockRelease(*lastPartitionLock); + *lastPartitionLock = NULL; + } + + /* + * Must lock the lower-numbered partition first to avoid + * deadlocks. + */ + if (oldPartitionLock < newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + else if (oldPartitionLock > newPartitionLock) + { + Assert(*lastPartitionLock == NULL); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE); + } + else + { + /* only one partition, only one lock */ + if (*lastPartitionLock != newPartitionLock) + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + } + else + { + /* if it wasn't valid, we need only the new partition */ + if (*lastPartitionLock != newPartitionLock) + { + if (*lastPartitionLock) + LWLockRelease(*lastPartitionLock); + LWLockAcquire(newPartitionLock, LW_EXCLUSIVE); + } + + /* remember we have no old-partition lock or tag */ + oldPartitionLock = NULL; + /* keep the compiler quiet about uninitialized variables */ + oldHash = 0; + } + + *lastPartitionLock = newPartitionLock; + + /* + * Try to make a hashtable entry for the buffer under its new tag. + * This could fail because while we were writing someone else + * allocated another buffer for the same block we want to read in. + * Note that we have not yet removed the hashtable entry for the old + * tag. + */ + buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); + Assert(buf_id < 0); + + /* + * Need to lock the buffer header too in order to change its tag. + */ + buf_state = LockBufHdr(buf); + + /* + * Somebody could have pinned or re-dirtied the buffer while we were + * doing the I/O and making the new hashtable entry. If so, we can't + * recycle this buffer; we must undo everything we've done and start + * over with a new victim buffer. + */ + oldFlags = buf_state & BUF_FLAG_MASK; + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY)) + break; + + pg_unreachable(); + } + + /* + * Okay, it's finally safe to rename the buffer. + * + * Clearing BM_VALID here is necessary, clearing the dirtybits is just + * paranoia. We also reset the usage_count since any recency of use of + * the old content is no longer relevant. (The usage_count starts out at + * 1 so that the buffer can survive one clock-sweep pass.) + * + * Make sure BM_PERMANENT is set for buffers that must be written at every + * checkpoint. Unlogged buffers only need to be written at shutdown + * checkpoints, except for their "init" forks, which need to be treated + * just like permanent relations. + */ + buf->tag = newTag; + buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | + BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT | + BUF_USAGECOUNT_MASK); + if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) + buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE; + else + buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + + UnlockBufHdr(buf, buf_state); + + if (oldPartitionLock != NULL) + { + BufTableDelete(&oldTag, oldHash); + if (oldPartitionLock != newPartitionLock) + LWLockRelease(oldPartitionLock); + } + + /* + * Buffer contents are currently invalid. Try to get the io_in_progress + * lock. If StartBufferIO returns false, then someone else managed to + * read it before we did, so there's nothing left for BufferAlloc() to do. + */ + StartBufferIO(buf, true); + + return buf; +} + + /* * InvalidateBuffer -- mark a shared buffer invalid and return it to the * freelist. diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 3377fa5676..277705f18c 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -124,7 +124,7 @@ typedef struct buftag * NB: NUM_BUFFER_PARTITIONS must be a power of 2! */ #define BufTableHashPartition(hashcode) \ - ((hashcode) % NUM_BUFFER_PARTITIONS) + ((hashcode >> 7) % NUM_BUFFER_PARTITIONS) #define BufMappingPartitionLock(hashcode) \ (&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + \ BufTableHashPartition(hashcode)].lock) -- 2.25.1 --------------065E078E0383D7C686F94091-- ^ permalink raw reply [nested|flat] 23+ messages in thread
* NOT ENFORCED constraint feature @ 2024-10-08 09:06 Amul Sul <[email protected]> 0 siblings, 0 replies; 23+ messages in thread From: Amul Sul @ 2024-10-08 09:06 UTC (permalink / raw) To: pgsql-hackers Hi, In SQL Standard 2023, a table's check or referential constraint can be either ENFORCED or NOT ENFORCED. Currently, when a DML statement is executed, all enforced constraints are validated. If any constraint is violated, an exception is raised, and the SQL transaction is rolled back. These are referred to as ENFORCED constraints. On the other hand, a NOT ENFORCED constraint is a rule defined in the database but not checked when data is inserted or updated. This can help speed up large data imports, improve performance when strict validation isn't required, or handle cases where constraints are enforced externally (e.g., by application logic). It also allows the rule to be documented without enforcing it during normal operations. The attached patch proposes adding the ability to define CHECK and FOREIGN KEY constraints as NOT ENFORCED. If neither ENFORCED nor NOT ENFORCED is explicitly specified when defining a constraint, the default setting is that the constraint is ENFORCED. Note that this addition differs from the properties of NOT VALID and DEFERRABLE constraints, which skip checks only for existing data and determine when to perform checks, respectively. In contrast, NOT ENFORCED completely skips the checks altogether. Adding NOT ENFORCED to CHECK constraints is simple, see 0001 patch, but implementing it for FOREIGN KEY constraints requires more code due to triggers, see 0002 - 0005 patches. There are various approaches for implementing NOT ENFORCED foreign keys, what I thought of: 1. When defining a NOT ENFORCED foreign key, skip the creation of triggers used for referential integrity check, while defining an ENFORCED foreign key, remain the same as the current behaviour. If an existing foreign key is changed to NOT ENFORCED, the triggers are dropped, and when switching it back to ENFORCED, the triggers are recreated. 2. Another approach could be to create the NOT ENFORCED constraint with the triggers as usual, but disable those triggers by updating the pg_trigger catalog so that they are never executed for the check. And enable them when the constraint is changed back to ENFORCED. 3. Similarly, a final approach would involve updating the logic where trigger execution is decided and skipping the execution if the constraint is not enforced, rather than modifying the pg_trigger catalog. In the attached patch, the first approach has been implemented. This requires more code changes but prevents unused triggers from being left in the database and avoids the need for changes all over the place to skip trigger execution, which could be missed in future code additions. The ALTER CONSTRAINT operation in the patch added code to handle dropping and recreating triggers. An alternative approach would be to simplify the process by dropping and recreating the FK constraint, which would automatically handle skipping or creating triggers for NOT ENFORCED or ENFORCED FK constraints. However, I wasn't sure if this was the right approach, as I couldn't find any existing ALTER operations that follow this pattern. Also note that the existing CHECK constraints currently do not support ALTER operations. This functionality may be essential for modifying a constraint's enforcement status; otherwise, users must drop and recreate the CHECK constraint to change its enforceability. I have not yet begun work on this, as it would involve significant code refactoring and updates to the documentation. I plan to start this once we finalise the design and reach a common understanding regarding this proposal. Any comments, suggestions, or assistance would be greatly appreciated. Thank you. -- Regards, Amul Sul EDB: http://www.enterprisedb.com Attachments: [application/x-patch] v1-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patch (42.9K, ../../CAAJ_b962c5AcYW9KUt_R_ER5qs3fUGbe4az-SP-vuwPS-w-AGA@mail.gmail.com/2-v1-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patch) download | inline diff: From 75d25c63d0b10d6439ade792899d6adb8f99d660 Mon Sep 17 00:00:00 2001 From: Amul Sul <[email protected]> Date: Fri, 4 Oct 2024 18:22:42 +0530 Subject: [PATCH v1 1/5] Add support for NOT ENFORCED in CHECK constraints. This added basic infrastructure for the NOT ENFORCED/ENFORCED constraint supports where incluing grammar and catalog flags. Note that CHECK constraints do not currently support ALTER operations, so changing the enforceability of an existing constraint isn't possible without dropping and recreating it. We could consider adding support for altering CHECK constraints either in this patch series or as a separatly. --- doc/src/sgml/ddl.sgml | 8 ++- doc/src/sgml/ref/alter_table.sgml | 14 +++-- doc/src/sgml/ref/create_table.sgml | 18 +++++- src/backend/access/common/tupdesc.c | 4 +- src/backend/catalog/heap.c | 13 ++-- src/backend/catalog/index.c | 1 + src/backend/catalog/pg_constraint.c | 5 ++ src/backend/commands/tablecmds.c | 56 ++++++++++++------ src/backend/commands/trigger.c | 1 + src/backend/commands/typecmds.c | 14 +++++ src/backend/executor/execMain.c | 8 ++- src/backend/parser/gram.y | 72 ++++++++++++++++++----- src/backend/parser/parse_utilcmd.c | 39 ++++++++++++ src/backend/utils/adt/ruleutils.c | 7 +++ src/backend/utils/cache/relcache.c | 1 + src/include/access/tupdesc.h | 1 + src/include/catalog/heap.h | 1 + src/include/catalog/pg_constraint.h | 2 + src/include/nodes/parsenodes.h | 3 + src/include/parser/kwlist.h | 1 + src/test/regress/expected/constraints.out | 22 ++++++- src/test/regress/sql/constraints.sql | 15 ++++- 22 files changed, 253 insertions(+), 53 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 8ab0ddb112f..892a74346ca 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -581,7 +581,10 @@ CREATE TABLE products ( consists of the key word <literal>CHECK</literal> followed by an expression in parentheses. The check constraint expression should involve the column thus constrained, otherwise the constraint - would not make too much sense. + would not make too much sense. The check constraint expression can be + followed by the <literal>ENFORCED</literal>, which is the default, or + <literal>NOT ENFORCED</literal> keyword, determining whether the + constraint check is performed or skipped after each statement. </para> <indexterm> @@ -717,7 +720,8 @@ CREATE TABLE products ( to implement that. (This approach avoids the dump/restore problem because <application>pg_dump</application> does not reinstall triggers until after restoring data, so that the check will not be enforced during a - dump/restore.) + dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>, + allowing the underlying constraint rule to be represented without being enforced. </para> </note> diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 36770c012a6..257f871d99e 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable> ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ] ADD <replaceable class="parameter">table_constraint_using_index</replaceable> - ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] + ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ] VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ] DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ] @@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> | REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] } -[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] +[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ] <phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase> @@ -119,7 +119,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] | FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] } -[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] +[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ] <phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase> @@ -1427,9 +1427,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM </para> <para> - Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires - scanning the table to verify that existing rows meet the constraint, - but does not require a table rewrite. + Adding a enforced <literal>CHECK</literal> or <literal>NOT NULL</literal> + constraint requires scanning the table to verify that existing rows meet the + constraint, but does not require a table rewrite. If a <literal>CHECK</literal> + constraint is added as <literal>NOT ENFORCED</literal>, the validation will + not be performed. </para> <para> diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 83859bac76f..55b51602136 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> | REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] } -[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] +[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ] <phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase> @@ -83,7 +83,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] } -[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] +[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ] <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase> @@ -1362,6 +1362,20 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM </listitem> </varlistentry> + <varlistentry id="sql-createtable-parms-enforce"> + <term><literal>ENFORCED</literal></term> + <term><literal>NOT ENFORCED</literal></term> + <listitem> + <para> + This is currently only allowed for <literal>CHECK</literal> constraints. + If the constraint is <literal>NOT ENFORCED</literal>, this clause + specifies that the constraint check will be skipped. When the constraint + is <literal>ENFORCED</literal>, check is performed after each statement. + This is the default. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createtable-method"> <term><literal>USING <replaceable class="parameter">method</replaceable></literal></term> <listitem> diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c index 47379fef220..41dd00e5547 100644 --- a/src/backend/access/common/tupdesc.c +++ b/src/backend/access/common/tupdesc.c @@ -226,6 +226,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc) cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin); cpy->check[i].ccvalid = constr->check[i].ccvalid; cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit; + cpy->check[i].ccenforced = constr->check[i].ccenforced; } } @@ -548,7 +549,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2) if (!(strcmp(check1->ccname, check2->ccname) == 0 && strcmp(check1->ccbin, check2->ccbin) == 0 && check1->ccvalid == check2->ccvalid && - check1->ccnoinherit == check2->ccnoinherit)) + check1->ccnoinherit == check2->ccnoinherit && + check1->ccenforced == check2->ccenforced)) return false; } } diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 78e59384d1c..7aa8cb8055a 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -103,7 +103,8 @@ static ObjectAddress AddNewRelationType(const char *typeName, static void RelationRemoveInheritance(Oid relid); static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr, bool is_validated, bool is_local, int inhcount, - bool is_no_inherit, bool is_internal); + bool is_no_inherit, bool is_enforced, + bool is_internal); static void StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal); static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, @@ -2073,7 +2074,7 @@ SetAttrMissing(Oid relid, char *attname, char *value) static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr, bool is_validated, bool is_local, int inhcount, - bool is_no_inherit, bool is_internal) + bool is_no_inherit, bool is_enforced, bool is_internal) { char *ccbin; List *varList; @@ -2139,6 +2140,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, false, /* Is Deferrable */ false, /* Is Deferred */ is_validated, + is_enforced, /* Is enforced */ InvalidOid, /* no parent constraint */ RelationGetRelid(rel), /* relation */ attNos, /* attrs in the constraint */ @@ -2212,7 +2214,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal) StoreRelCheck(rel, con->name, con->expr, !con->skip_validation, con->is_local, con->inhcount, con->is_no_inherit, - is_internal); + con->is_enforced, is_internal); numchecks++; break; default: @@ -2345,6 +2347,7 @@ AddRelationNewConstraints(Relation rel, cooked->attnum = colDef->attnum; cooked->expr = expr; cooked->skip_validation = false; + cooked->is_enforced = true; cooked->is_local = is_local; cooked->inhcount = is_local ? 0 : 1; cooked->is_no_inherit = false; @@ -2463,7 +2466,8 @@ AddRelationNewConstraints(Relation rel, */ constrOid = StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local, - is_local ? 0 : 1, cdef->is_no_inherit, is_internal); + is_local ? 0 : 1, cdef->is_no_inherit, + cdef->is_enforced, is_internal); numchecks++; @@ -2474,6 +2478,7 @@ AddRelationNewConstraints(Relation rel, cooked->attnum = 0; cooked->expr = expr; cooked->skip_validation = cdef->skip_validation; + cooked->is_enforced = cdef->is_enforced; cooked->is_local = is_local; cooked->inhcount = is_local ? 0 : 1; cooked->is_no_inherit = cdef->is_no_inherit; diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 6084dfa97cb..edef21e88ab 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1958,6 +1958,7 @@ index_constraint_create(Relation heapRelation, deferrable, initdeferred, true, + true, /* Is enforced */ parentConstraintId, RelationGetRelid(heapRelation), indexInfo->ii_IndexAttrNumbers, diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 1e2df031a84..14468b2b7d0 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -52,6 +52,7 @@ CreateConstraintEntry(const char *constraintName, bool isDeferrable, bool isDeferred, bool isValidated, + bool isEnforced, Oid parentConstrId, Oid relId, const int16 *constraintKey, @@ -97,6 +98,9 @@ CreateConstraintEntry(const char *constraintName, ObjectAddresses *addrs_auto; ObjectAddresses *addrs_normal; + /* Only CHECK constraint can be not enforced */ + Assert(isEnforced || constraintType == CONSTRAINT_CHECK); + conDesc = table_open(ConstraintRelationId, RowExclusiveLock); Assert(constraintName); @@ -181,6 +185,7 @@ CreateConstraintEntry(const char *constraintName, values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable); values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred); values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated); + values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced); values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId); values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId); values[Anum_pg_constraint_conindid - 1] = ObjectIdGetDatum(indexRelId); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index af8c05b91f1..6f7123e5f71 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -363,7 +363,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation, Oid relId, Oid oldRelId, void *arg); static List *MergeAttributes(List *columns, const List *supers, char relpersistence, bool is_partition, List **supconstr); -static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr); +static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced); static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef); static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef); static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition); @@ -946,6 +946,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, cooked->attnum = attnum; cooked->expr = colDef->cooked_default; cooked->skip_validation = false; + cooked->is_enforced = true; cooked->is_local = true; /* not used for defaults */ cooked->inhcount = 0; /* ditto */ cooked->is_no_inherit = false; @@ -2824,7 +2825,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, name, RelationGetRelationName(relation)))); - constraints = MergeCheckConstraint(constraints, name, expr); + constraints = MergeCheckConstraint(constraints, name, expr, + check[i].ccenforced); } } @@ -3026,7 +3028,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, * the list. */ static List * -MergeCheckConstraint(List *constraints, const char *name, Node *expr) +MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced) { ListCell *lc; CookedConstraint *newcon; @@ -3067,6 +3069,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr) newcon->name = pstrdup(name); newcon->expr = expr; newcon->inhcount = 1; + newcon->is_enforced = is_enforced; return lappend(constraints, newcon); } @@ -9463,6 +9466,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, { CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon); + /* Only CHECK constraint can be not enforced */ + Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK); + if (!ccon->skip_validation) { NewConstraint *newcon; @@ -10226,6 +10232,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, fkconstraint->deferrable, fkconstraint->initdeferred, fkconstraint->initially_valid, + true, /* Is enforced */ parentConstr, RelationGetRelid(rel), fkattnum, @@ -10528,6 +10535,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, fkconstraint->deferrable, fkconstraint->initdeferred, fkconstraint->initially_valid, + true, /* Is enforced */ parentConstr, partitionId, mapped_fkattnum, @@ -11055,6 +11063,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) fkconstraint->deferrable, fkconstraint->initdeferred, constrForm->convalidated, + true, /* Is enforced */ parentConstrOid, RelationGetRelid(partRel), mapped_conkey, @@ -11821,22 +11830,29 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, table_close(childrel, NoLock); } - /* Queue validation for phase 3 */ - newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); - newcon->name = constrName; - newcon->contype = CONSTR_CHECK; - newcon->refrelid = InvalidOid; - newcon->refindid = InvalidOid; - newcon->conid = con->oid; + /* + * Queue validation for phase 3 only if constraint is enforced; + * otherwise, adding it to the validation queue won't be very + * effective, as the verification will be skipped. + */ + if (con->conenforced) + { + newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon->name = constrName; + newcon->contype = CONSTR_CHECK; + newcon->refrelid = InvalidOid; + newcon->refindid = InvalidOid; + newcon->conid = con->oid; - val = SysCacheGetAttrNotNull(CONSTROID, tuple, - Anum_pg_constraint_conbin); - conbin = TextDatumGetCString(val); - newcon->qual = (Node *) stringToNode(conbin); + val = SysCacheGetAttrNotNull(CONSTROID, tuple, + Anum_pg_constraint_conbin); + conbin = TextDatumGetCString(val); + newcon->qual = (Node *) stringToNode(conbin); - /* Find or create work queue entry for this table */ - tab = ATGetQueueEntry(wqueue, rel); - tab->constraints = lappend(tab->constraints, newcon); + /* Find or create work queue entry for this table */ + tab = ATGetQueueEntry(wqueue, rel); + tab->constraints = lappend(tab->constraints, newcon); + } /* * Invalidate relcache so that others see the new validated @@ -11846,7 +11862,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, } /* - * Now update the catalog, while we have the door open. + * Now update the catalog regardless of enforcement; the validated + * flag will not take effect until the constraint is marked as + * enforced. */ copyTuple = heap_copytuple(tuple); copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); @@ -15851,6 +15869,7 @@ constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc) if (acon->condeferrable != bcon->condeferrable || acon->condeferred != bcon->condeferred || + acon->conenforced != bcon->conenforced || strcmp(decompile_conbin(a, tupleDesc), decompile_conbin(b, tupleDesc)) != 0) return false; @@ -19610,6 +19629,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel) n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr)); n->initially_valid = true; n->skip_validation = true; + n->is_enforced = true; /* It's a re-add, since it nominally already exists */ ATAddCheckConstraint(wqueue, tab, partRel, n, true, false, true, ShareUpdateExclusiveLock); diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 3671e82535e..bce023e5329 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -810,6 +810,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, stmt->deferrable, stmt->initdeferred, true, + true, /* Is enforced */ InvalidOid, /* no parent */ RelationGetRelid(rel), NULL, /* no conkey */ diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 2a6550de907..e77fd2f140a 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -1007,6 +1007,12 @@ DefineDomain(CreateDomainStmt *stmt) errmsg("specifying constraint deferrability not supported for domains"))); break; + case CONSTR_ATTR_NOT_ENFORCED: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("specifying not enforced constraint not supported for domains"))); + break; + default: elog(ERROR, "unrecognized constraint subtype: %d", (int) constr->contype); @@ -2967,6 +2973,12 @@ AlterDomainAddConstraint(List *names, Node *newConstraint, errmsg("specifying constraint deferrability not supported for domains"))); break; + case CONSTR_ATTR_NOT_ENFORCED: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("specifying not enforced constraint not supported for domains"))); + break; + default: elog(ERROR, "unrecognized constraint subtype: %d", (int) constr->contype); @@ -3597,6 +3609,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid, false, /* Is Deferrable */ false, /* Is Deferred */ !constr->skip_validation, /* Is Validated */ + true, /* Is enforced */ InvalidOid, /* no parent constraint */ InvalidOid, /* not a relation constraint */ NULL, @@ -3704,6 +3717,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid, false, /* Is Deferrable */ false, /* Is Deferred */ !constr->skip_validation, /* Is Validated */ + true, /* Is enforced */ InvalidOid, /* no parent constraint */ InvalidOid, /* not a relation constraint */ NULL, diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index cc9a594cba5..cec69afe188 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -1746,11 +1746,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo, { oldContext = MemoryContextSwitchTo(estate->es_query_cxt); resultRelInfo->ri_ConstraintExprs = - (ExprState **) palloc(ncheck * sizeof(ExprState *)); + (ExprState **) palloc0(ncheck * sizeof(ExprState *)); for (i = 0; i < ncheck; i++) { Expr *checkconstr; + /* Skip not enforced constraint */ + if (!check[i].ccenforced) + continue; + checkconstr = stringToNode(check[i].ccbin); resultRelInfo->ri_ConstraintExprs[i] = ExecPrepareExpr(checkconstr, estate); @@ -1777,7 +1781,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo, * is not to be treated as a failure. Therefore, use ExecCheck not * ExecQual. */ - if (!ExecCheck(checkconstr, econtext)) + if (checkconstr && !ExecCheck(checkconstr, econtext)) return check[i].ccname; } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 4aa8646af7b..437a0b9f2f0 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -158,6 +158,8 @@ typedef struct KeyActions #define CAS_INITIALLY_DEFERRED 0x08 #define CAS_NOT_VALID 0x10 #define CAS_NO_INHERIT 0x20 +#define CAS_NOT_ENFORCED 0x40 +#define CAS_ENFORCED 0x80 #define parser_yyerror(msg) scanner_yyerror(msg, yyscanner) @@ -211,7 +213,7 @@ static void SplitColQualList(List *qualList, core_yyscan_t yyscanner); static void processCASbits(int cas_bits, int location, const char *constrType, bool *deferrable, bool *initdeferred, bool *not_valid, - bool *no_inherit, core_yyscan_t yyscanner); + bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner); static PartitionStrategy parsePartitionStrategy(char *strategy); static void preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner); @@ -724,9 +726,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE - EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION - EXTENSION EXTERNAL EXTRACT + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P + ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN + EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS @@ -2677,7 +2679,7 @@ alter_table_cmd: processCASbits($4, @4, "ALTER CONSTRAINT statement", &c->deferrable, &c->initdeferred, - NULL, NULL, yyscanner); + NULL, NULL, &c->is_enforced, yyscanner); $$ = (Node *) n; } /* ALTER TABLE <name> VALIDATE CONSTRAINT ... */ @@ -3976,6 +3978,7 @@ ColConstraintElem: n->cooked_expr = NULL; n->skip_validation = false; n->initially_valid = true; + n->is_enforced = true; $$ = (Node *) n; } | DEFAULT b_expr @@ -4100,6 +4103,22 @@ ConstraintAttr: n->location = @1; $$ = (Node *) n; } + | ENFORCED + { + Constraint *n = makeNode(Constraint); + + n->contype = CONSTR_ATTR_ENFORCED; + n->location = @1; + $$ = (Node *) n; + } + | NOT ENFORCED + { + Constraint *n = makeNode(Constraint); + + n->contype = CONSTR_ATTR_NOT_ENFORCED; + n->location = @1; + $$ = (Node *) n; + } ; @@ -4162,7 +4181,7 @@ ConstraintElem: n->cooked_expr = NULL; processCASbits($5, @5, "CHECK", NULL, NULL, &n->skip_validation, - &n->is_no_inherit, yyscanner); + &n->is_no_inherit, &n->is_enforced, yyscanner); n->initially_valid = !n->skip_validation; $$ = (Node *) n; } @@ -4182,7 +4201,7 @@ ConstraintElem: n->indexspace = $9; processCASbits($10, @10, "UNIQUE", &n->deferrable, &n->initdeferred, NULL, - NULL, yyscanner); + NULL, NULL, yyscanner); $$ = (Node *) n; } | UNIQUE ExistingIndex ConstraintAttributeSpec @@ -4198,7 +4217,7 @@ ConstraintElem: n->indexspace = NULL; processCASbits($3, @3, "UNIQUE", &n->deferrable, &n->initdeferred, NULL, - NULL, yyscanner); + NULL, NULL, yyscanner); $$ = (Node *) n; } | PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace @@ -4216,7 +4235,7 @@ ConstraintElem: n->indexspace = $9; processCASbits($10, @10, "PRIMARY KEY", &n->deferrable, &n->initdeferred, NULL, - NULL, yyscanner); + NULL, NULL, yyscanner); $$ = (Node *) n; } | PRIMARY KEY ExistingIndex ConstraintAttributeSpec @@ -4232,7 +4251,7 @@ ConstraintElem: n->indexspace = NULL; processCASbits($4, @4, "PRIMARY KEY", &n->deferrable, &n->initdeferred, NULL, - NULL, yyscanner); + NULL, NULL, yyscanner); $$ = (Node *) n; } | EXCLUDE access_method_clause '(' ExclusionConstraintList ')' @@ -4252,7 +4271,7 @@ ConstraintElem: n->where_clause = $9; processCASbits($10, @10, "EXCLUDE", &n->deferrable, &n->initdeferred, NULL, - NULL, yyscanner); + NULL, NULL, yyscanner); $$ = (Node *) n; } | FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name @@ -4282,6 +4301,7 @@ ConstraintElem: processCASbits($12, @12, "FOREIGN KEY", &n->deferrable, &n->initdeferred, &n->skip_validation, NULL, + NULL, yyscanner); n->initially_valid = !n->skip_validation; $$ = (Node *) n; @@ -4322,7 +4342,7 @@ DomainConstraintElem: n->cooked_expr = NULL; processCASbits($5, @5, "CHECK", NULL, NULL, &n->skip_validation, - &n->is_no_inherit, yyscanner); + &n->is_no_inherit, NULL, yyscanner); n->initially_valid = !n->skip_validation; $$ = (Node *) n; } @@ -4336,7 +4356,7 @@ DomainConstraintElem: /* no NOT VALID support yet */ processCASbits($3, @3, "NOT NULL", NULL, NULL, NULL, - &n->is_no_inherit, yyscanner); + &n->is_no_inherit, NULL, yyscanner); n->initially_valid = true; $$ = (Node *) n; } @@ -5998,7 +6018,7 @@ CreateTrigStmt: n->transitionRels = NIL; processCASbits($11, @11, "TRIGGER", &n->deferrable, &n->initdeferred, NULL, - NULL, yyscanner); + NULL, NULL, yyscanner); n->constrrel = $10; $$ = (Node *) n; } @@ -6167,7 +6187,8 @@ ConstraintAttributeSpec: parser_errposition(@2))); /* generic message for other conflicts */ if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) || - (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) + (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) || + (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting constraint properties"), @@ -6183,6 +6204,8 @@ ConstraintAttributeElem: | INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; } | NOT VALID { $$ = CAS_NOT_VALID; } | NO INHERIT { $$ = CAS_NO_INHERIT; } + | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; } + | ENFORCED { $$ = CAS_ENFORCED; } ; @@ -17636,6 +17659,7 @@ unreserved_keyword: | ENABLE_P | ENCODING | ENCRYPTED + | ENFORCED | ENUM_P | ERROR_P | ESCAPE @@ -18213,6 +18237,7 @@ bare_label_keyword: | ENCODING | ENCRYPTED | END_P + | ENFORCED | ENUM_P | ERROR_P | ESCAPE @@ -19306,7 +19331,7 @@ SplitColQualList(List *qualList, static void processCASbits(int cas_bits, int location, const char *constrType, bool *deferrable, bool *initdeferred, bool *not_valid, - bool *no_inherit, core_yyscan_t yyscanner) + bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner) { /* defaults */ if (deferrable) @@ -19315,6 +19340,8 @@ processCASbits(int cas_bits, int location, const char *constrType, *initdeferred = false; if (not_valid) *not_valid = false; + if (is_enforced) + *is_enforced = true; if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED)) { @@ -19367,6 +19394,19 @@ processCASbits(int cas_bits, int location, const char *constrType, constrType), parser_errposition(location))); } + + if (cas_bits & CAS_NOT_ENFORCED) + { + if (is_enforced) + *is_enforced = false; + else + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + /* translator: %s is CHECK, UNIQUE, or similar */ + errmsg("%s constraints cannot be marked NOT ENFORCED", + constrType), + parser_errposition(location))); + } } /* diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 1e15ce10b48..024cc256b7d 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -835,6 +835,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) case CONSTR_ATTR_NOT_DEFERRABLE: case CONSTR_ATTR_DEFERRED: case CONSTR_ATTR_IMMEDIATE: + case CONSTR_ATTR_ENFORCED: + case CONSTR_ATTR_NOT_ENFORCED: /* transformConstraintAttrs took care of these */ break; @@ -955,6 +957,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint) case CONSTR_ATTR_NOT_DEFERRABLE: case CONSTR_ATTR_DEFERRED: case CONSTR_ATTR_IMMEDIATE: + case CONSTR_ATTR_ENFORCED: + case CONSTR_ATTR_NOT_ENFORCED: elog(ERROR, "invalid context for constraint type %d", constraint->contype); break; @@ -1317,6 +1321,7 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause) n->is_no_inherit = ccnoinherit; n->raw_expr = NULL; n->cooked_expr = nodeToString(ccbin_node); + n->is_enforced = true; /* We can skip validation, since the new table should be empty. */ n->skip_validation = true; @@ -3715,6 +3720,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) Constraint *lastprimarycon = NULL; bool saw_deferrability = false; bool saw_initially = false; + bool saw_enforced = false; ListCell *clist; #define SUPPORTS_ATTRS(node) \ @@ -3810,12 +3816,45 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) lastprimarycon->initdeferred = false; break; + case CONSTR_ATTR_ENFORCED: + if (lastprimarycon && + lastprimarycon->contype != CONSTR_CHECK) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("misplaced ENFORCED clause"), + parser_errposition(cxt->pstate, con->location))); + if (saw_enforced) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"), + parser_errposition(cxt->pstate, con->location))); + saw_enforced = true; + lastprimarycon->is_enforced = true; + break; + + case CONSTR_ATTR_NOT_ENFORCED: + if (lastprimarycon && + lastprimarycon->contype != CONSTR_CHECK) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("misplaced NOT ENFORCED clause"), + parser_errposition(cxt->pstate, con->location))); + if (saw_enforced) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"), + parser_errposition(cxt->pstate, con->location))); + saw_enforced = true; + lastprimarycon->is_enforced = false; + break; + default: /* Otherwise it's not an attribute */ lastprimarycon = con; /* reset flags for new primary node */ saw_deferrability = false; saw_initially = false; + saw_enforced = false; break; } } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2177d17e278..cfdc4e31cd4 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2372,6 +2372,9 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, appendStringInfoChar(&buf, ')'); } + if (!conForm->conenforced) + appendStringInfoString(&buf, " NOT ENFORCED"); + break; } case CONSTRAINT_PRIMARY: @@ -2514,6 +2517,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, appendStringInfo(&buf, "CHECK (%s)%s", consrc, conForm->connoinherit ? " NO INHERIT" : ""); + + if (!conForm->conenforced) + appendStringInfoString(&buf, " NOT ENFORCED"); + break; } case CONSTRAINT_TRIGGER: diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index c326f687eb4..a603b74c778 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4624,6 +4624,7 @@ CheckConstraintFetch(Relation relation) } check[found].ccvalid = conform->convalidated; + check[found].ccenforced = conform->conenforced; check[found].ccnoinherit = conform->connoinherit; check[found].ccname = MemoryContextStrdup(CacheMemoryContext, NameStr(conform->conname)); diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h index 8930a28d660..96bac018083 100644 --- a/src/include/access/tupdesc.h +++ b/src/include/access/tupdesc.h @@ -30,6 +30,7 @@ typedef struct ConstrCheck char *ccname; char *ccbin; /* nodeToString representation of expr */ bool ccvalid; + bool ccenforced; bool ccnoinherit; /* this is a non-inheritable constraint */ } ConstrCheck; diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index c512824cd1c..96b22b4f86c 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -40,6 +40,7 @@ typedef struct CookedConstraint AttrNumber attnum; /* which attr (only for DEFAULT) */ Node *expr; /* transformed default or check expr */ bool skip_validation; /* skip validation? (only for CHECK) */ + bool is_enforced; /* is enforced? (only for CHECK) */ bool is_local; /* constraint has local (non-inherited) def */ int inhcount; /* number of times constraint is inherited */ bool is_no_inherit; /* constraint has local def and cannot be diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index 115217a6162..22074cd9e8a 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -52,6 +52,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) bool condeferrable; /* deferrable constraint? */ bool condeferred; /* deferred by default? */ bool convalidated; /* constraint has been validated? */ + bool conenforced; /* enforced constraint? */ /* * conrelid and conkey are only meaningful if the constraint applies to a @@ -223,6 +224,7 @@ extern Oid CreateConstraintEntry(const char *constraintName, bool isDeferrable, bool isDeferred, bool isValidated, + bool isEnforced, Oid parentConstrId, Oid relId, const int16 *constraintKey, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 1c314cd9074..c29002edb24 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2725,6 +2725,8 @@ typedef enum ConstrType /* types of constraints */ CONSTR_ATTR_NOT_DEFERRABLE, CONSTR_ATTR_DEFERRED, CONSTR_ATTR_IMMEDIATE, + CONSTR_ATTR_ENFORCED, + CONSTR_ATTR_NOT_ENFORCED, } ConstrType; /* Foreign key action codes */ @@ -2748,6 +2750,7 @@ typedef struct Constraint bool initdeferred; /* INITIALLY DEFERRED? */ bool skip_validation; /* skip validation of existing rows? */ bool initially_valid; /* mark the new constraint as valid? */ + bool is_enforced; /* enforced constraint? */ bool is_no_inherit; /* is constraint non-inheritable? */ Node *raw_expr; /* CHECK or DEFAULT expression, as * untransformed parse tree */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 899d64ad55f..f2bc378394e 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index cf0b80d6169..7fc1ced7efc 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -86,6 +86,25 @@ SELECT * FROM CHECK_TBL; 6 (3 rows) +CREATE TABLE NE_CHECK_TBL (x int, + CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED); +INSERT INTO NE_CHECK_TBL VALUES (5); +INSERT INTO NE_CHECK_TBL VALUES (4); +INSERT INTO NE_CHECK_TBL VALUES (3); +INSERT INTO NE_CHECK_TBL VALUES (2); +INSERT INTO NE_CHECK_TBL VALUES (6); +INSERT INTO NE_CHECK_TBL VALUES (1); +SELECT * FROM NE_CHECK_TBL; + x +--- + 5 + 4 + 3 + 2 + 6 + 1 +(6 rows) + CREATE SEQUENCE CHECK_SEQ; CREATE TABLE CHECK2_TBL (x int, y text, z int, CONSTRAINT SEQUENCE_CON @@ -119,7 +138,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'), y TEXT DEFAULT '-NULL-', z INT DEFAULT -1 * currval('insert_seq'), CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8), - CHECK (x + z = 0)); + CHECK (x + z = 0) ENFORCED, /* no change it is a default */ + CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED); INSERT INTO INSERT_TBL(x,z) VALUES (2, -2); ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con" DETAIL: Failing row contains (2, -NULL-, -2). diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index e3e3bea7091..dac4f03bd71 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -66,6 +66,18 @@ INSERT INTO CHECK_TBL VALUES (1); SELECT * FROM CHECK_TBL; +CREATE TABLE NE_CHECK_TBL (x int, + CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED); + +INSERT INTO NE_CHECK_TBL VALUES (5); +INSERT INTO NE_CHECK_TBL VALUES (4); +INSERT INTO NE_CHECK_TBL VALUES (3); +INSERT INTO NE_CHECK_TBL VALUES (2); +INSERT INTO NE_CHECK_TBL VALUES (6); +INSERT INTO NE_CHECK_TBL VALUES (1); + +SELECT * FROM NE_CHECK_TBL; + CREATE SEQUENCE CHECK_SEQ; CREATE TABLE CHECK2_TBL (x int, y text, z int, @@ -91,7 +103,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'), y TEXT DEFAULT '-NULL-', z INT DEFAULT -1 * currval('insert_seq'), CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8), - CHECK (x + z = 0)); + CHECK (x + z = 0) ENFORCED, /* no change it is a default */ + CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED); INSERT INTO INSERT_TBL(x,z) VALUES (2, -2); -- 2.18.0 [application/x-patch] v1-0002-refactor-split-ATExecAlterConstrRecurse.patch (8.3K, ../../CAAJ_b962c5AcYW9KUt_R_ER5qs3fUGbe4az-SP-vuwPS-w-AGA@mail.gmail.com/3-v1-0002-refactor-split-ATExecAlterConstrRecurse.patch) download | inline diff: From e71dc15381a3e7876de6fb39a8c3ab67f090d89b Mon Sep 17 00:00:00 2001 From: Amul Sul <[email protected]> Date: Mon, 23 Sep 2024 12:31:07 +0530 Subject: [PATCH v1 2/5] refactor: split ATExecAlterConstrRecurse() When changing a NOT ENFORCED foreign key constraint to ENFORCED, we need to create the necessary triggers for enforcing the constraint. We can set the deferrability at the time of creation and skip the code used for other constraint alteration operations. Additionally, move the code that iterates over each child constraint of the constraint being altered into a separate function. --- src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------ 1 file changed, 118 insertions(+), 76 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 6f7123e5f71..fe786f02304 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -384,6 +384,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, Relation rel, HeapTuple contuple, List **otherrelids, LOCKMODE lockmode); +static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, + bool deferrable, bool initdeferred, + List **otherrelids); +static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel, + Relation rel, HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode); static ObjectAddress ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, bool recurse, bool recursing, LOCKMODE lockmode); @@ -11577,9 +11583,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, { HeapTuple copyTuple; Form_pg_constraint copy_con; - HeapTuple tgtuple; - ScanKeyData tgkey; - SysScanDesc tgscan; copyTuple = heap_copytuple(contuple); copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); @@ -11600,53 +11603,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, * Now we need to update the multiple entries in pg_trigger that * implement the constraint. */ - ScanKeyInit(&tgkey, - Anum_pg_trigger_tgconstraint, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conoid)); - tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true, - NULL, 1, &tgkey); - while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan))) - { - Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple); - Form_pg_trigger copy_tg; - HeapTuple tgCopyTuple; - - /* - * Remember OIDs of other relation(s) involved in FK constraint. - * (Note: it's likely that we could skip forcing a relcache inval - * for other rels that don't have a trigger whose properties - * change, but let's be conservative.) - */ - if (tgform->tgrelid != RelationGetRelid(rel)) - *otherrelids = list_append_unique_oid(*otherrelids, - tgform->tgrelid); - - /* - * Update deferrability of RI_FKey_noaction_del, - * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd - * triggers, but not others; see createForeignKeyActionTriggers - * and CreateFKCheckTrigger. - */ - if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL && - tgform->tgfoid != F_RI_FKEY_NOACTION_UPD && - tgform->tgfoid != F_RI_FKEY_CHECK_INS && - tgform->tgfoid != F_RI_FKEY_CHECK_UPD) - continue; - - tgCopyTuple = heap_copytuple(tgtuple); - copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple); - - copy_tg->tgdeferrable = cmdcon->deferrable; - copy_tg->tginitdeferred = cmdcon->initdeferred; - CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple); - - InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0); - - heap_freetuple(tgCopyTuple); - } - - systable_endscan(tgscan); + AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable, + cmdcon->initdeferred, otherrelids); } /* @@ -11659,36 +11617,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, */ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE) - { - ScanKeyData pkey; - SysScanDesc pscan; - HeapTuple childtup; - - ScanKeyInit(&pkey, - Anum_pg_constraint_conparentid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conoid)); - - pscan = systable_beginscan(conrel, ConstraintParentIndexId, - true, NULL, 1, &pkey); - - while (HeapTupleIsValid(childtup = systable_getnext(pscan))) - { - Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup); - Relation childrel; - - childrel = table_open(childcon->conrelid, lockmode); - ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup, - otherrelids, lockmode); - table_close(childrel, NoLock); - } - - systable_endscan(pscan); - } + ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple, + otherrelids, lockmode); return changed; } +/* + * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's + * deferrability. + * + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterConstrRecurse. + */ +static void +AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, + bool deferrable, bool initdeferred, + List **otherrelids) +{ + HeapTuple tgtuple; + ScanKeyData tgkey; + SysScanDesc tgscan; + + ScanKeyInit(&tgkey, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true, + NULL, 1, &tgkey); + while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan))) + { + Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple); + Form_pg_trigger copy_tg; + HeapTuple tgCopyTuple; + + /* + * Remember OIDs of other relation(s) involved in FK constraint. + * (Note: it's likely that we could skip forcing a relcache inval for + * other rels that don't have a trigger whose properties change, but + * let's be conservative.) + */ + if (tgform->tgrelid != RelationGetRelid(rel)) + *otherrelids = list_append_unique_oid(*otherrelids, + tgform->tgrelid); + + /* + * Update enable status and deferrability of RI_FKey_noaction_del, + * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd + * triggers, but not others; see createForeignKeyActionTriggers and + * CreateFKCheckTrigger. + */ + if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL && + tgform->tgfoid != F_RI_FKEY_NOACTION_UPD && + tgform->tgfoid != F_RI_FKEY_CHECK_INS && + tgform->tgfoid != F_RI_FKEY_CHECK_UPD) + continue; + + tgCopyTuple = heap_copytuple(tgtuple); + copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple); + + copy_tg->tgdeferrable = deferrable; + copy_tg->tginitdeferred = initdeferred; + CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple); + + InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0); + + heap_freetuple(tgCopyTuple); + } + + systable_endscan(tgscan); +} + +/* + * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the + * specified constraint. + * + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterConstrRecurse. + */ +static void +ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel, + Relation rel, HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode) +{ + Form_pg_constraint currcon; + Oid conoid; + ScanKeyData pkey; + SysScanDesc pscan; + HeapTuple childtup; + + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + conoid = currcon->oid; + + ScanKeyInit(&pkey, + Anum_pg_constraint_conparentid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + + pscan = systable_beginscan(conrel, ConstraintParentIndexId, + true, NULL, 1, &pkey); + + while (HeapTupleIsValid(childtup = systable_getnext(pscan))) + { + Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup); + Relation childrel; + + childrel = table_open(childcon->conrelid, lockmode); + ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup, + otherrelids, lockmode); + table_close(childrel, NoLock); + } + + systable_endscan(pscan); +} + /* * ALTER TABLE VALIDATE CONSTRAINT * -- 2.18.0 [application/x-patch] v1-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patch (6.2K, ../../CAAJ_b962c5AcYW9KUt_R_ER5qs3fUGbe4az-SP-vuwPS-w-AGA@mail.gmail.com/4-v1-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patch) download | inline diff: From dca36ab357d57c908d5b6c263d039110b47be212 Mon Sep 17 00:00:00 2001 From: Amul Sul <[email protected]> Date: Mon, 23 Sep 2024 12:37:05 +0530 Subject: [PATCH v1 3/5] refactor: Change ATExecAlterConstrRecurse() argument. Instead of passing the Relation of the referencing relation, we will pass a relid. Opening the relation using the relid again should not cause significant issues. However, this approach makes recursion more efficient, especially in the later patch where we will be recreating the referencing and referred triggers. In addition to that passing relid of the referred relation too and Oids of triggers. ---- NOTE: This patch is not meant to be committed separately. It should be squashed with the main patch that adds ENFORCED/NOT ENFORCED. ---- --- src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index fe786f02304..f6e527b0a69 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -382,14 +382,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel, static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode); static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, - Relation rel, HeapTuple contuple, List **otherrelids, - LOCKMODE lockmode); + const Oid fkrelid, const Oid pkrelid, + HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode, Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger); static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, bool deferrable, bool initdeferred, List **otherrelids); static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel, - Relation rel, HeapTuple contuple, List **otherrelids, - LOCKMODE lockmode); + const Oid fkrelid, const Oid pkrelid, + HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode, Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger); static ObjectAddress ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, bool recurse, bool recursing, LOCKMODE lockmode); @@ -11523,8 +11531,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse, currcon->condeferred != cmdcon->initdeferred || rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { - if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple, - &otherrelids, lockmode)) + if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid, + currcon->confrelid, contuple, &otherrelids, + lockmode, InvalidOid, InvalidOid, + InvalidOid, InvalidOid)) ObjectAddressSet(address, ConstraintRelationId, currcon->oid); } @@ -11557,13 +11567,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse, */ static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, - Relation rel, HeapTuple contuple, List **otherrelids, - LOCKMODE lockmode) + const Oid fkrelid, const Oid pkrelid, + HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode, Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger) { Form_pg_constraint currcon; Oid conoid; Oid refrelid; bool changed = false; + Relation rel; /* since this function recurses, it could be driven to stack overflow */ check_stack_depth(); @@ -11572,6 +11587,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, conoid = currcon->oid; refrelid = currcon->confrelid; + rel = table_open(currcon->conrelid, lockmode); + /* * Update pg_constraint with the flags from cmdcon. * @@ -11617,8 +11634,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, */ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE) - ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple, - otherrelids, lockmode); + ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid, + contuple, otherrelids, lockmode, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger); + + table_close(rel, NoLock); return changed; } @@ -11697,8 +11720,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, */ static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel, - Relation rel, HeapTuple contuple, List **otherrelids, - LOCKMODE lockmode) + const Oid fkrelid, const Oid pkrelid, + HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode, Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger) { Form_pg_constraint currcon; Oid conoid; @@ -11718,15 +11745,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel, true, NULL, 1, &pkey); while (HeapTupleIsValid(childtup = systable_getnext(pscan))) - { - Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup); - Relation childrel; - - childrel = table_open(childcon->conrelid, lockmode); - ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup, - otherrelids, lockmode); - table_close(childrel, NoLock); - } + ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid, + childtup, otherrelids, lockmode, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger); systable_endscan(pscan); } -- 2.18.0 [application/x-patch] v1-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patch (10.5K, ../../CAAJ_b962c5AcYW9KUt_R_ER5qs3fUGbe4az-SP-vuwPS-w-AGA@mail.gmail.com/5-v1-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patch) download | inline diff: From f2d762fc846c6c3dfd04258c1a6f64d7a78a747a Mon Sep 17 00:00:00 2001 From: Amul Sul <[email protected]> Date: Mon, 7 Oct 2024 12:40:07 +0530 Subject: [PATCH v1 4/5] Remove hastriggers flag check before fetching FK constraints. With NOT ENFORCED, foreign key constraints will be created without triggers. Therefore, the criteria for fetching foreign keys based on the presence of triggers no longer apply. ---- NOTE: This patch is intended to reduce the diff noise from the main patch and is not meant to be committed separately. It should be squashed with the main patch that adds ENFORCED/NOT ENFORCED. ---- --- src/backend/utils/cache/relcache.c | 5 - src/bin/pg_dump/pg_dump.c | 8 +- src/bin/psql/describe.c | 223 ++++++++++++++--------------- 3 files changed, 107 insertions(+), 129 deletions(-) diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index a603b74c778..711a5290729 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4707,11 +4707,6 @@ RelationGetFKeyList(Relation relation) if (relation->rd_fkeyvalid) return relation->rd_fkeylist; - /* Fast path: non-partitioned tables without triggers can't have FKs */ - if (!relation->rd_rel->relhastriggers && - relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) - return NIL; - /* * We build the list we intend to return (in the caller's context) while * doing the scan. After successfully completing the scan, we copy that diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 1b47c388ced..7225bb3c466 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -7823,13 +7823,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables) { TableInfo *tinfo = &tblinfo[i]; - /* - * For partitioned tables, foreign keys have no triggers so they must - * be included anyway in case some foreign keys are defined. - */ - if ((!tinfo->hastriggers && - tinfo->relkind != RELKIND_PARTITIONED_TABLE) || - !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)) + if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)) continue; /* OK, we need info for this table */ diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 6a36c910833..478fc3476f9 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -2503,135 +2503,124 @@ describeOneTableDetails(const char *schemaname, } /* - * Print foreign-key constraints (there are none if no triggers, - * except if the table is partitioned, in which case the triggers - * appear in the partitions) + * Print foreign-key constraints */ - if (tableinfo.hastriggers || - tableinfo.relkind == RELKIND_PARTITIONED_TABLE) + if (pset.sversion >= 120000 && + (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE)) { - if (pset.sversion >= 120000 && - (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE)) + /* + * Put the constraints defined in this table first, followed + * by the constraints defined in ancestor partitioned tables. + */ + printfPQExpBuffer(&buf, + "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n" + " conname,\n" + " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n" + " conrelid::pg_catalog.regclass AS ontable\n" + " FROM pg_catalog.pg_constraint,\n" + " pg_catalog.pg_partition_ancestors('%s')\n" + " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n" + "ORDER BY sametable DESC, conname;", + oid, oid); + } + else + { + printfPQExpBuffer(&buf, + "SELECT true as sametable, conname,\n" + " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n" + " conrelid::pg_catalog.regclass AS ontable\n" + "FROM pg_catalog.pg_constraint r\n" + "WHERE r.conrelid = '%s' AND r.contype = 'f'\n", + oid); + + if (pset.sversion >= 120000) + appendPQExpBufferStr(&buf, " AND conparentid = 0\n"); + appendPQExpBufferStr(&buf, "ORDER BY conname"); + } + + result = PSQLexec(buf.data); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + { + int i_sametable = PQfnumber(result, "sametable"), + i_conname = PQfnumber(result, "conname"), + i_condef = PQfnumber(result, "condef"), + i_ontable = PQfnumber(result, "ontable"); + + printTableAddFooter(&cont, _("Foreign-key constraints:")); + for (i = 0; i < tuples; i++) { /* - * Put the constraints defined in this table first, followed - * by the constraints defined in ancestor partitioned tables. + * Print untranslated constraint name and definition. Use + * a "TABLE tab" prefix when the constraint is defined in + * a parent partitioned table. */ - printfPQExpBuffer(&buf, - "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n" - " conname,\n" - " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n" - " conrelid::pg_catalog.regclass AS ontable\n" - " FROM pg_catalog.pg_constraint,\n" - " pg_catalog.pg_partition_ancestors('%s')\n" - " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n" - "ORDER BY sametable DESC, conname;", - oid, oid); - } - else - { - printfPQExpBuffer(&buf, - "SELECT true as sametable, conname,\n" - " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n" - " conrelid::pg_catalog.regclass AS ontable\n" - "FROM pg_catalog.pg_constraint r\n" - "WHERE r.conrelid = '%s' AND r.contype = 'f'\n", - oid); - - if (pset.sversion >= 120000) - appendPQExpBufferStr(&buf, " AND conparentid = 0\n"); - appendPQExpBufferStr(&buf, "ORDER BY conname"); - } - - result = PSQLexec(buf.data); - if (!result) - goto error_return; - else - tuples = PQntuples(result); - - if (tuples > 0) - { - int i_sametable = PQfnumber(result, "sametable"), - i_conname = PQfnumber(result, "conname"), - i_condef = PQfnumber(result, "condef"), - i_ontable = PQfnumber(result, "ontable"); - - printTableAddFooter(&cont, _("Foreign-key constraints:")); - for (i = 0; i < tuples; i++) - { - /* - * Print untranslated constraint name and definition. Use - * a "TABLE tab" prefix when the constraint is defined in - * a parent partitioned table. - */ - if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0) - printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s", - PQgetvalue(result, i, i_ontable), - PQgetvalue(result, i, i_conname), - PQgetvalue(result, i, i_condef)); - else - printfPQExpBuffer(&buf, " \"%s\" %s", - PQgetvalue(result, i, i_conname), - PQgetvalue(result, i, i_condef)); - - printTableAddFooter(&cont, buf.data); - } - } - PQclear(result); - } - - /* print incoming foreign-key references */ - if (tableinfo.hastriggers || - tableinfo.relkind == RELKIND_PARTITIONED_TABLE) - { - if (pset.sversion >= 120000) - { - printfPQExpBuffer(&buf, - "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n" - " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n" - " FROM pg_catalog.pg_constraint c\n" - " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n" - " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n" - " AND contype = 'f' AND conparentid = 0\n" - "ORDER BY conname;", - oid, oid); - } - else - { - printfPQExpBuffer(&buf, - "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n" - " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n" - " FROM pg_catalog.pg_constraint\n" - " WHERE confrelid = %s AND contype = 'f'\n" - "ORDER BY conname;", - oid); - } - - result = PSQLexec(buf.data); - if (!result) - goto error_return; - else - tuples = PQntuples(result); - - if (tuples > 0) - { - int i_conname = PQfnumber(result, "conname"), - i_ontable = PQfnumber(result, "ontable"), - i_condef = PQfnumber(result, "condef"); - - printTableAddFooter(&cont, _("Referenced by:")); - for (i = 0; i < tuples; i++) - { + if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0) printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s", PQgetvalue(result, i, i_ontable), PQgetvalue(result, i, i_conname), PQgetvalue(result, i, i_condef)); + else + printfPQExpBuffer(&buf, " \"%s\" %s", + PQgetvalue(result, i, i_conname), + PQgetvalue(result, i, i_condef)); - printTableAddFooter(&cont, buf.data); - } + printTableAddFooter(&cont, buf.data); } - PQclear(result); } + PQclear(result); + + if (pset.sversion >= 120000) + { + printfPQExpBuffer(&buf, + "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n" + " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n" + " FROM pg_catalog.pg_constraint c\n" + " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n" + " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n" + " AND contype = 'f' AND conparentid = 0\n" + "ORDER BY conname;", + oid, oid); + } + else + { + printfPQExpBuffer(&buf, + "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n" + " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n" + " FROM pg_catalog.pg_constraint\n" + " WHERE confrelid = %s AND contype = 'f'\n" + "ORDER BY conname;", + oid); + } + + result = PSQLexec(buf.data); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + { + int i_conname = PQfnumber(result, "conname"), + i_ontable = PQfnumber(result, "ontable"), + i_condef = PQfnumber(result, "condef"); + + printTableAddFooter(&cont, _("Referenced by:")); + for (i = 0; i < tuples; i++) + { + printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s", + PQgetvalue(result, i, i_ontable), + PQgetvalue(result, i, i_conname), + PQgetvalue(result, i, i_condef)); + + printTableAddFooter(&cont, buf.data); + } + } + PQclear(result); /* print any row-level policies */ if (pset.sversion >= 90500) -- 2.18.0 [application/x-patch] v1-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patch (55.5K, ../../CAAJ_b962c5AcYW9KUt_R_ER5qs3fUGbe4az-SP-vuwPS-w-AGA@mail.gmail.com/6-v1-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patch) download | inline diff: From 70a5ffa86fc8133778fcdc9e8178db0eec5795d1 Mon Sep 17 00:00:00 2001 From: Amul Sul <[email protected]> Date: Tue, 8 Oct 2024 11:35:17 +0530 Subject: [PATCH v1 5/5] Add support for NOT ENFORCED in foreign key constraints. Normally, when a foreign key (FK) constraint is created on a table, action/check triggers are associated with the constraint to ensure data integrity. With this patch, when constraints are marked as NOT ENFORCED, integrity checks are not required, and thus, these triggers are unnecessary. As a result, when creating a NOT ENFORCED FK constraint or changing an existing FK constraint to NOT ENFORCED, the creation of triggers will be skipped, or existing triggers will be dropped, respectively. And when changing an existing NOT ENFORCED FK constraint to ENFORCED, those triggers will be created. --- doc/src/sgml/advanced.sgml | 13 + doc/src/sgml/ddl.sgml | 12 +- doc/src/sgml/ref/alter_table.sgml | 5 + doc/src/sgml/ref/create_table.sgml | 10 +- src/backend/catalog/pg_constraint.c | 5 +- src/backend/commands/tablecmds.c | 538 +++++++++++++++------- src/backend/parser/gram.y | 3 +- src/backend/parser/parse_utilcmd.c | 6 +- src/backend/utils/cache/relcache.c | 1 + src/include/utils/rel.h | 3 + src/test/regress/expected/foreign_key.out | 118 ++++- src/test/regress/sql/foreign_key.sql | 87 +++- 12 files changed, 620 insertions(+), 181 deletions(-) diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml index 755c9f14850..b7d90de69d7 100644 --- a/doc/src/sgml/advanced.sgml +++ b/doc/src/sgml/advanced.sgml @@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea DETAIL: Key (city)=(Berkeley) is not present in table "cities". </screen> </para> + <para> + If, for some reason, you do not want to enforce this constraint and wish to + avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>, + or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>. + </para> + <para> + Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you + attempt to reinsert the same record, it will not result in any error. +<programlisting> +ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED; +INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28'); +</programlisting> + </para> <para> The behavior of foreign keys can be finely tuned to your diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 892a74346ca..c718e2d8f0d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1030,11 +1030,13 @@ CREATE TABLE example ( </indexterm> <para> - A foreign key constraint specifies that the values in a column (or - a group of columns) must match the values appearing in some row - of another table. - We say this maintains the <firstterm>referential - integrity</firstterm> between two related tables. + A foreign key constraint with <literal>ENFORCED</literal>, the default + setting, specifies that the values in a column (or a group of columns) + must match the values appearing in some row of another table. We say this + maintains the <firstterm>referential integrity</firstterm> between two + related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>, + the check for aforementioned <firstterm>referential integrity</firstterm> + will not be performed. </para> <para> diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 257f871d99e..26ef967a66b 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -554,6 +554,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM This form alters the attributes of a constraint that was previously created. Currently only foreign key constraints may be altered. </para> + <para> + Note that changing constraints to <literal>ENFORCED</literal>, which were + previously marked as valid and <literal>NOT ENFORCED</literal>, will + trigger validation, similar to <literal>VALIDATE CONSTRAINT</literal>. + </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 55b51602136..77452efe740 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -1367,11 +1367,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM <term><literal>NOT ENFORCED</literal></term> <listitem> <para> - This is currently only allowed for <literal>CHECK</literal> constraints. - If the constraint is <literal>NOT ENFORCED</literal>, this clause - specifies that the constraint check will be skipped. When the constraint - is <literal>ENFORCED</literal>, check is performed after each statement. - This is the default. + This is currently only allowed for foreign key and <literal>CHECK</literal> + constraints. If the constraint is <literal>NOT ENFORCED</literal>, this + clause specifies that the constraint check will be skipped. When the + constraint is <literal>ENFORCED</literal>, check is performed after each + statement. This is the default. </para> </listitem> </varlistentry> diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 14468b2b7d0..69d645761d7 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -98,8 +98,9 @@ CreateConstraintEntry(const char *constraintName, ObjectAddresses *addrs_auto; ObjectAddresses *addrs_normal; - /* Only CHECK constraint can be not enforced */ - Assert(isEnforced || constraintType == CONSTRAINT_CHECK); + /* Only CHECK or FOREIGN KEY constraint can be not enforced */ + Assert(isEnforced || constraintType == CONSTRAINT_CHECK || + constraintType == CONSTRAINT_FOREIGN); conDesc = table_open(ConstraintRelationId, RowExclusiveLock); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index f6e527b0a69..d7f6eb53a1d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -379,19 +379,29 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel, static void AlterSeqNamespaces(Relation classRel, Relation rel, Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved, LOCKMODE lockmode); -static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, +static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode); -static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, +static bool ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, + Relation conrel, Relation tgrel, const Oid fkrelid, const Oid pkrelid, HeapTuple contuple, List **otherrelids, LOCKMODE lockmode, Oid ReferencedParentDelTrigger, Oid ReferencedParentUpdTrigger, Oid ReferencingParentInsTrigger, Oid ReferencingParentUpdTrigger); -static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, +static void ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon, + Relation conrel, Relation tgrel, + const Oid fkrelid, const Oid pkrelid, + HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode, Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger); +static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel, bool deferrable, bool initdeferred, List **otherrelids); -static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel, +static void ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, + Relation conrel, Relation tgrel, const Oid fkrelid, const Oid pkrelid, HeapTuple contuple, List **otherrelids, LOCKMODE lockmode, Oid ReferencedParentDelTrigger, @@ -5359,7 +5369,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, lockmode); break; case AT_AlterConstraint: /* ALTER CONSTRAINT */ - address = ATExecAlterConstraint(rel, cmd, false, false, lockmode); + address = ATExecAlterConstraint(wqueue, rel, cmd, false, false, lockmode); break; case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */ address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse, @@ -9480,8 +9490,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, { CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon); - /* Only CHECK constraint can be not enforced */ - Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK); + /* Only CHECK or FOREIGN KEY constraint can be not enforced */ + Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK || + ccon->contype == CONSTRAINT_FOREIGN); if (!ccon->skip_validation) { @@ -10192,8 +10203,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, bool conislocal; int coninhcount; bool connoinherit; - Oid deleteTriggerOid, - updateTriggerOid; + Oid deleteTriggerOid = InvalidOid, + updateTriggerOid = InvalidOid; /* * Verify relkind for each referenced partition. At the top level, this @@ -10246,7 +10257,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, fkconstraint->deferrable, fkconstraint->initdeferred, fkconstraint->initially_valid, - true, /* Is enforced */ + fkconstraint->is_enforced, parentConstr, RelationGetRelid(rel), fkattnum, @@ -10294,13 +10305,15 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, CommandCounterIncrement(); /* - * Create the action triggers that enforce the constraint. + * Create action triggers to enforce the constraint, or skip them if the + * constraint is not enforced. */ - createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel), - fkconstraint, - constrOid, indexOid, - parentDelTrigger, parentUpdTrigger, - &deleteTriggerOid, &updateTriggerOid); + if (fkconstraint->is_enforced) + createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel), + fkconstraint, + constrOid, indexOid, + parentDelTrigger, parentUpdTrigger, + &deleteTriggerOid, &updateTriggerOid); /* * If the referenced table is partitioned, recurse on ourselves to handle @@ -10409,8 +10422,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, Oid parentInsTrigger, Oid parentUpdTrigger, bool with_period) { - Oid insertTriggerOid, - updateTriggerOid; + Oid insertTriggerOid = InvalidOid, + updateTriggerOid = InvalidOid; Assert(OidIsValid(parentConstr)); @@ -10420,29 +10433,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, errmsg("foreign key constraints are not supported on foreign tables"))); /* - * Add the check triggers to it and, if necessary, schedule it to be - * checked in Phase 3. + * Add check triggers if the constraint is enforced, and if needed, + * schedule them to be checked in Phase 3. * * If the relation is partitioned, drill down to do it to its partitions. */ - createForeignKeyCheckTriggers(RelationGetRelid(rel), - RelationGetRelid(pkrel), - fkconstraint, - parentConstr, - indexOid, - parentInsTrigger, parentUpdTrigger, - &insertTriggerOid, &updateTriggerOid); + if (fkconstraint->is_enforced) + createForeignKeyCheckTriggers(RelationGetRelid(rel), + RelationGetRelid(pkrel), + fkconstraint, + parentConstr, + indexOid, + parentInsTrigger, parentUpdTrigger, + &insertTriggerOid, &updateTriggerOid); if (rel->rd_rel->relkind == RELKIND_RELATION) { /* * Tell Phase 3 to check that the constraint is satisfied by existing - * rows. We can skip this during table creation, when requested - * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command, - * and when we're recreating a constraint following a SET DATA TYPE - * operation that did not impugn its validity. + * rows. We can skip this during table creation, when constraint is + * specified as NOT ENFORCED, when requested explicitly by specifying + * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating + * a constraint following a SET DATA TYPE operation that did not + * impugn its validity. */ - if (wqueue && !old_check_ok && !fkconstraint->skip_validation) + if (wqueue && !old_check_ok && !fkconstraint->skip_validation && + fkconstraint->is_enforced) { NewConstraint *newcon; AlteredTableInfo *tab; @@ -10549,7 +10565,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, fkconstraint->deferrable, fkconstraint->initdeferred, fkconstraint->initially_valid, - true, /* Is enforced */ + fkconstraint->is_enforced, parentConstr, partitionId, mapped_fkattnum, @@ -10727,8 +10743,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) int numfkdelsetcols; AttrNumber confdelsetcols[INDEX_MAX_KEYS]; Constraint *fkconstraint; - Oid deleteTriggerOid, - updateTriggerOid; + Oid deleteTriggerOid = InvalidOid, + updateTriggerOid = InvalidOid; tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid)); if (!HeapTupleIsValid(tuple)) @@ -10783,6 +10799,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) fkconstraint->conname = NameStr(constrForm->conname); fkconstraint->deferrable = constrForm->condeferrable; fkconstraint->initdeferred = constrForm->condeferred; + fkconstraint->is_enforced = constrForm->conenforced; fkconstraint->location = -1; fkconstraint->pktable = NULL; /* ->fk_attrs determined below */ @@ -10822,9 +10839,11 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) * parent OIDs for similar triggers that will be created on the * partition in addFkRecurseReferenced(). */ - GetForeignKeyActionTriggers(trigrel, constrOid, - constrForm->confrelid, constrForm->conrelid, - &deleteTriggerOid, &updateTriggerOid); + if (constrForm->conenforced) + GetForeignKeyActionTriggers(trigrel, constrOid, + constrForm->confrelid, + constrForm->conrelid, + &deleteTriggerOid, &updateTriggerOid); addFkRecurseReferenced(NULL, fkconstraint, @@ -10984,17 +11003,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) mapped_conkey[i] = attmap->attnums[conkey[i] - 1]; /* - * Get the "check" triggers belonging to the constraint to pass as - * parent OIDs for similar triggers that will be created on the - * partition in addFkRecurseReferencing(). They are also passed to - * tryAttachPartitionForeignKey() below to simply assign as parents to - * the partition's existing "check" triggers, that is, if the - * corresponding constraints is deemed attachable to the parent - * constraint. + * Get the "check" triggers belonging to the constraint, if it is + * enforced, to pass as parent OIDs for similar triggers that will be + * created on the partition in addFkRecurseReferencing(). They are + * also passed to tryAttachPartitionForeignKey() below to simply + * assign as parents to the partition's existing "check" triggers, + * that is, if the corresponding constraints is deemed attachable to + * the parent constraint. */ - GetForeignKeyCheckTriggers(trigrel, constrForm->oid, - constrForm->confrelid, constrForm->conrelid, - &insertTriggerOid, &updateTriggerOid); + if (constrForm->conenforced) + GetForeignKeyCheckTriggers(trigrel, constrForm->oid, + constrForm->confrelid, constrForm->conrelid, + &insertTriggerOid, &updateTriggerOid); /* * Before creating a new constraint, see whether any existing FKs are @@ -11036,6 +11056,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) /* ->conname determined below */ fkconstraint->deferrable = constrForm->condeferrable; fkconstraint->initdeferred = constrForm->condeferred; + fkconstraint->is_enforced = constrForm->conenforced; fkconstraint->location = -1; fkconstraint->pktable = NULL; /* ->fk_attrs determined below */ @@ -11077,7 +11098,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) fkconstraint->deferrable, fkconstraint->initdeferred, constrForm->convalidated, - true, /* Is enforced */ + fkconstraint->is_enforced, parentConstrOid, RelationGetRelid(partRel), mapped_conkey, @@ -11217,6 +11238,7 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk, !partConstr->convalidated || partConstr->condeferrable != parentConstr->condeferrable || partConstr->condeferred != parentConstr->condeferred || + partConstr->conenforced != parentConstr->conenforced || partConstr->confupdtype != parentConstr->confupdtype || partConstr->confdeltype != parentConstr->confdeltype || partConstr->confmatchtype != parentConstr->confmatchtype) @@ -11276,18 +11298,22 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk, ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid); /* - * Like the constraint, attach partition's "check" triggers to the - * corresponding parent triggers. + * Similar to the constraint, attach the partition's "check" triggers to + * the corresponding parent triggers if the constraint is enforced. + * Non-enforced constraints do not have these triggers. */ - GetForeignKeyCheckTriggers(trigrel, - fk->conoid, fk->confrelid, fk->conrelid, - &insertTriggerOid, &updateTriggerOid); - Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger)); - TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger, - partRelid); - Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger)); - TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger, - partRelid); + if (fk->conenforced) + { + GetForeignKeyCheckTriggers(trigrel, + fk->conoid, fk->confrelid, fk->conrelid, + &insertTriggerOid, &updateTriggerOid); + Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger)); + TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger, + partRelid); + Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger)); + TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger, + partRelid); + } CommandCounterIncrement(); return true; @@ -11426,8 +11452,8 @@ GetForeignKeyCheckTriggers(Relation trigrel, * InvalidObjectAddress. */ static ObjectAddress -ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse, - bool recursing, LOCKMODE lockmode) +ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd, + bool recurse, bool recursing, LOCKMODE lockmode) { Constraint *cmdcon; Relation conrel; @@ -11529,12 +11555,14 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse, address = InvalidObjectAddress; if (currcon->condeferrable != cmdcon->deferrable || currcon->condeferred != cmdcon->initdeferred || + currcon->conenforced != cmdcon->is_enforced || rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { - if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid, - currcon->confrelid, contuple, &otherrelids, - lockmode, InvalidOid, InvalidOid, - InvalidOid, InvalidOid)) + if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, + currcon->conrelid, currcon->confrelid, + contuple, &otherrelids, lockmode, + InvalidOid, InvalidOid, InvalidOid, + InvalidOid)) ObjectAddressSet(address, ConstraintRelationId, currcon->oid); } @@ -11566,8 +11594,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse, * but existing releases don't do that.) */ static bool -ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, - const Oid fkrelid, const Oid pkrelid, +ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, Relation conrel, + Relation tgrel, const Oid fkrelid, const Oid pkrelid, HeapTuple contuple, List **otherrelids, LOCKMODE lockmode, Oid ReferencedParentDelTrigger, Oid ReferencedParentUpdTrigger, @@ -11576,19 +11604,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, { Form_pg_constraint currcon; Oid conoid; + Oid conrelid; Oid refrelid; bool changed = false; - Relation rel; /* since this function recurses, it could be driven to stack overflow */ check_stack_depth(); currcon = (Form_pg_constraint) GETSTRUCT(contuple); conoid = currcon->oid; + conrelid = currcon->conrelid; refrelid = currcon->confrelid; - rel = table_open(currcon->conrelid, lockmode); - /* * Update pg_constraint with the flags from cmdcon. * @@ -11596,7 +11623,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, * silently do nothing. */ if (currcon->condeferrable != cmdcon->deferrable || - currcon->condeferred != cmdcon->initdeferred) + currcon->condeferred != cmdcon->initdeferred || + currcon->conenforced != cmdcon->is_enforced) { HeapTuple copyTuple; Form_pg_constraint copy_con; @@ -11605,6 +11633,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); copy_con->condeferrable = cmdcon->deferrable; copy_con->condeferred = cmdcon->initdeferred; + copy_con->conenforced = cmdcon->is_enforced; CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); InvokeObjectPostAlterHook(ConstraintRelationId, @@ -11614,38 +11643,214 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, changed = true; /* Make new constraint flags visible to others */ - CacheInvalidateRelcache(rel); + CacheInvalidateRelcacheByRelid(conrelid); + } + if (currcon->conenforced != cmdcon->is_enforced) + { + ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid, + pkrelid, contuple, otherrelids, lockmode, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger); + } + else + { /* * Now we need to update the multiple entries in pg_trigger that * implement the constraint. */ - AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable, - cmdcon->initdeferred, otherrelids); + AlterConstrTriggerDeferrability(conoid, tgrel, conrelid, + cmdcon->deferrable, + cmdcon->initdeferred, + otherrelids); + + /* + * If the table at either end of the constraint is partitioned, we + * need to recurse and handle every constraint that is a child of this + * one. + * + * (This assumes that the recurse flag is forcibly set for partitioned + * tables, and not set for legacy inheritance, though we don't check + * for that here.) + */ + if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE || + get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE) + ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid, + pkrelid, contuple, otherrelids, lockmode, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger); } - /* - * If the table at either end of the constraint is partitioned, we need to - * recurse and handle every constraint that is a child of this one. - * - * (This assumes that the recurse flag is forcibly set for partitioned - * tables, and not set for legacy inheritance, though we don't check for - * that here.) - */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || - get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE) - ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid, - contuple, otherrelids, lockmode, - ReferencedParentDelTrigger, - ReferencedParentUpdTrigger, - ReferencingParentInsTrigger, - ReferencingParentUpdTrigger); - - table_close(rel, NoLock); - return changed; } +/* + * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of + * a foreign key constraint. Depending on whether the constraint is being set + * to enforced or not enforced, it creates or drops the trigger accordingly. + * + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterConstrRecurse. + */ +static void +ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon, + Relation conrel, Relation tgrel, + const Oid fkrelid, const Oid pkrelid, + HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode, Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger) +{ + Form_pg_constraint currcon; + Oid conoid; + + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + conoid = currcon->oid; + + /* Drop triggers */ + if (!cmdcon->is_enforced) + { + HeapTuple tgtuple; + ScanKeyData tgkey; + SysScanDesc tgscan; + + /* + * When setting a constraint to NOT ENFORCED, the constraint triggers + * need to be dropped. Therefore, we must process the child relations + * first, followed by the parent, to account for dependencies. + */ + if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE || + get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE) + ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid, + pkrelid, contuple, otherrelids, lockmode, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger); + + ScanKeyInit(&tgkey, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true, + NULL, 1, &tgkey); + while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan))) + { + Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple); + ObjectAddress trigger; + + /* + * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd, + * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not + * others; see createForeignKeyActionTriggers and + * CreateFKCheckTrigger. + */ + if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL && + tgform->tgfoid != F_RI_FKEY_NOACTION_UPD && + tgform->tgfoid != F_RI_FKEY_CHECK_INS && + tgform->tgfoid != F_RI_FKEY_CHECK_UPD) + continue; + + deleteDependencyRecordsFor(TriggerRelationId, tgform->oid, + false); + /* make dependency deletion visible to performDeletion */ + CommandCounterIncrement(); + ObjectAddressSet(trigger, TriggerRelationId, tgform->oid); + performDeletion(&trigger, DROP_RESTRICT, 0); + /* make trigger drop visible, in case the loop iterates */ + CommandCounterIncrement(); + } + + systable_endscan(tgscan); + } + else /* Create triggers */ + { + Relation rel; + Oid ReferencedDelTriggerOid = InvalidOid, + ReferencedUpdTriggerOid = InvalidOid, + ReferencingInsTriggerOid = InvalidOid, + ReferencingUpdTriggerOid = InvalidOid; + + /* Update the foreign key action required for trigger creation. */ + cmdcon->fk_matchtype = currcon->confmatchtype; + cmdcon->fk_upd_action = currcon->confupdtype; + cmdcon->fk_del_action = currcon->confdeltype; + + rel = table_open(currcon->conrelid, lockmode); + + /* Create referenced triggers */ + if (currcon->conrelid == fkrelid) + createForeignKeyActionTriggers(rel, + currcon->confrelid, + cmdcon, + conoid, + currcon->conindid, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + &ReferencedDelTriggerOid, + &ReferencedUpdTriggerOid); + + /* Create referencing triggers */ + if (currcon->confrelid == pkrelid) + { + createForeignKeyCheckTriggers(currcon->conrelid, + pkrelid, + cmdcon, + conoid, + currcon->conindid, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger, + &ReferencingInsTriggerOid, + &ReferencingUpdTriggerOid); + + /* + * Tell Phase 3 to check that the constraint is satisfied by + * existing rows, but skip this step if the rows have not been + * validated previously. + */ + if (rel->rd_rel->relkind == RELKIND_RELATION && + wqueue && currcon->convalidated) + { + NewConstraint *newcon; + AlteredTableInfo *tab; + + tab = ATGetQueueEntry(wqueue, rel); + + newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon->name = pstrdup(NameStr(currcon->conname)); + newcon->conid = currcon->oid; + newcon->contype = CONSTR_FOREIGN; + newcon->refrelid = pkrelid; + newcon->refindid = currcon->conindid; + newcon->conwithperiod = currcon->conperiod; + newcon->qual = (Node *) cmdcon; + + tab->constraints = lappend(tab->constraints, newcon); + } + } + + /* + * If the table at either end of the constraint is partitioned, we + * need to recurse and create triggers for each constraint that is a + * child of this one. + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || + get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE) + ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid, + pkrelid, contuple, otherrelids, lockmode, + ReferencedDelTriggerOid, + ReferencedUpdTriggerOid, + ReferencingInsTriggerOid, + ReferencingUpdTriggerOid); + table_close(rel, NoLock); + } +} + /* * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's * deferrability. @@ -11654,7 +11859,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, * ATExecAlterConstrRecurse. */ static void -AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, +AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel, bool deferrable, bool initdeferred, List **otherrelids) { @@ -11680,7 +11885,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, * other rels that don't have a trigger whose properties change, but * let's be conservative.) */ - if (tgform->tgrelid != RelationGetRelid(rel)) + if (tgform->tgrelid != fkrel) *otherrelids = list_append_unique_oid(*otherrelids, tgform->tgrelid); @@ -11719,8 +11924,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, * ATExecAlterConstrRecurse. */ static void -ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel, - const Oid fkrelid, const Oid pkrelid, +ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, Relation conrel, + Relation tgrel, const Oid fkrelid, const Oid pkrelid, HeapTuple contuple, List **otherrelids, LOCKMODE lockmode, Oid ReferencedParentDelTrigger, Oid ReferencedParentUpdTrigger, @@ -11745,9 +11950,9 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel, true, NULL, 1, &pkey); while (HeapTupleIsValid(childtup = systable_getnext(pscan))) - ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid, - childtup, otherrelids, lockmode, - ReferencedParentDelTrigger, + ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, + fkrelid, pkrelid, childtup, otherrelids, + lockmode, ReferencedParentDelTrigger, ReferencedParentUpdTrigger, ReferencingParentInsTrigger, ReferencingParentUpdTrigger); @@ -11820,25 +12025,33 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, if (con->contype == CONSTRAINT_FOREIGN) { - NewConstraint *newcon; - Constraint *fkconstraint; + /* + * Queue validation for phase 3 only if constraint is enforced; + * otherwise, adding it to the validation queue won't be very + * effective, as the verification will be skipped. + */ + if (con->conenforced) + { + NewConstraint *newcon; + Constraint *fkconstraint; - /* Queue validation for phase 3 */ - fkconstraint = makeNode(Constraint); - /* for now this is all we need */ - fkconstraint->conname = constrName; + /* Queue validation for phase 3 */ + fkconstraint = makeNode(Constraint); + /* for now this is all we need */ + fkconstraint->conname = constrName; - newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); - newcon->name = constrName; - newcon->contype = CONSTR_FOREIGN; - newcon->refrelid = con->confrelid; - newcon->refindid = con->conindid; - newcon->conid = con->oid; - newcon->qual = (Node *) fkconstraint; + newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon->name = constrName; + newcon->contype = CONSTR_FOREIGN; + newcon->refrelid = con->confrelid; + newcon->refindid = con->conindid; + newcon->conid = con->oid; + newcon->qual = (Node *) fkconstraint; - /* Find or create work queue entry for this table */ - tab = ATGetQueueEntry(wqueue, rel); - tab->constraints = lappend(tab->constraints, newcon); + /* Find or create work queue entry for this table */ + tab = ATGetQueueEntry(wqueue, rel); + tab->constraints = lappend(tab->constraints, newcon); + } /* * We disallow creating invalid foreign keys to or from @@ -11897,9 +12110,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, } /* - * Queue validation for phase 3 only if constraint is enforced; - * otherwise, adding it to the validation queue won't be very - * effective, as the verification will be skipped. + * Queue validation for phase 3 only if the constraint is + * enforced, for the same reason outlined for the foreign key + * constraint. */ if (con->conenforced) { @@ -11930,7 +12143,10 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, /* * Now update the catalog regardless of enforcement; the validated * flag will not take effect until the constraint is marked as - * enforced. + * enforced. When changing a non-enforced constraint to enforced, the + * validation will only occur if the validation flag is true. + * Otherwise, the user will need to explicitly perform constraint + * validation. */ copyTuple = heap_copytuple(tuple); copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); @@ -19444,46 +19660,52 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, /* * Also, look up the partition's "check" triggers corresponding to the - * constraint being detached and detach them from the parent triggers. + * enforced constraint being detached and detach them from the parent + * triggers. Non-enforced constraints do not have these triggers; + * therefore, this step is not needed. */ - GetForeignKeyCheckTriggers(trigrel, - fk->conoid, fk->confrelid, fk->conrelid, - &insertTriggerOid, &updateTriggerOid); - Assert(OidIsValid(insertTriggerOid)); - TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, - RelationGetRelid(partRel)); - Assert(OidIsValid(updateTriggerOid)); - TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, - RelationGetRelid(partRel)); + if (fk->conenforced) + { + GetForeignKeyCheckTriggers(trigrel, + fk->conoid, fk->confrelid, fk->conrelid, + &insertTriggerOid, &updateTriggerOid); + Assert(OidIsValid(insertTriggerOid)); + TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, + RelationGetRelid(partRel)); + Assert(OidIsValid(updateTriggerOid)); + TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, + RelationGetRelid(partRel)); - /* - * Make the action triggers on the referenced relation. When this was - * a partition the action triggers pointed to the parent rel (they - * still do), but now we need separate ones of our own. - */ - fkconstraint = makeNode(Constraint); - fkconstraint->contype = CONSTRAINT_FOREIGN; - fkconstraint->conname = pstrdup(NameStr(conform->conname)); - fkconstraint->deferrable = conform->condeferrable; - fkconstraint->initdeferred = conform->condeferred; - fkconstraint->location = -1; - fkconstraint->pktable = NULL; - fkconstraint->fk_attrs = NIL; - fkconstraint->pk_attrs = NIL; - fkconstraint->fk_matchtype = conform->confmatchtype; - fkconstraint->fk_upd_action = conform->confupdtype; - fkconstraint->fk_del_action = conform->confdeltype; - fkconstraint->fk_del_set_cols = NIL; - fkconstraint->old_conpfeqop = NIL; - fkconstraint->old_pktable_oid = InvalidOid; - fkconstraint->skip_validation = false; - fkconstraint->initially_valid = true; + /* + * Make the action triggers on the referenced relation. When this + * was a partition the action triggers pointed to the parent rel + * (they still do), but now we need separate ones of our own. + */ + fkconstraint = makeNode(Constraint); + fkconstraint->contype = CONSTRAINT_FOREIGN; + fkconstraint->conname = pstrdup(NameStr(conform->conname)); + fkconstraint->deferrable = conform->condeferrable; + fkconstraint->initdeferred = conform->condeferred; + fkconstraint->is_enforced = conform->conenforced; + fkconstraint->location = -1; + fkconstraint->pktable = NULL; + fkconstraint->fk_attrs = NIL; + fkconstraint->pk_attrs = NIL; + fkconstraint->fk_matchtype = conform->confmatchtype; + fkconstraint->fk_upd_action = conform->confupdtype; + fkconstraint->fk_del_action = conform->confdeltype; + fkconstraint->fk_del_set_cols = NIL; + fkconstraint->old_conpfeqop = NIL; + fkconstraint->old_pktable_oid = InvalidOid; + fkconstraint->skip_validation = false; + fkconstraint->initially_valid = true; - createForeignKeyActionTriggers(partRel, conform->confrelid, - fkconstraint, fk->conoid, - conform->conindid, - InvalidOid, InvalidOid, - NULL, NULL); + createForeignKeyActionTriggers(partRel, conform->confrelid, + fkconstraint, fk->conoid, + conform->conindid, + InvalidOid, InvalidOid, + NULL, NULL); + } ReleaseSysCache(contup); } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 437a0b9f2f0..6a68e32d623 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -4040,6 +4040,7 @@ ColConstraintElem: n->fk_del_set_cols = ($5)->deleteAction->cols; n->skip_validation = false; n->initially_valid = true; + n->is_enforced = true; $$ = (Node *) n; } ; @@ -4301,7 +4302,7 @@ ConstraintElem: processCASbits($12, @12, "FOREIGN KEY", &n->deferrable, &n->initdeferred, &n->skip_validation, NULL, - NULL, + &n->is_enforced, yyscanner); n->initially_valid = !n->skip_validation; $$ = (Node *) n; diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 024cc256b7d..8dad6bfacb9 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -3818,7 +3818,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) case CONSTR_ATTR_ENFORCED: if (lastprimarycon && - lastprimarycon->contype != CONSTR_CHECK) + lastprimarycon->contype != CONSTR_CHECK && + lastprimarycon->contype != CONSTR_FOREIGN) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced ENFORCED clause"), @@ -3834,7 +3835,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) case CONSTR_ATTR_NOT_ENFORCED: if (lastprimarycon && - lastprimarycon->contype != CONSTR_CHECK) + lastprimarycon->contype != CONSTR_CHECK && + lastprimarycon->contype != CONSTR_FOREIGN) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced NOT ENFORCED clause"), diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 711a5290729..029676e0227 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4738,6 +4738,7 @@ RelationGetFKeyList(Relation relation) info->conoid = constraint->oid; info->conrelid = constraint->conrelid; info->confrelid = constraint->confrelid; + info->conenforced = constraint->conenforced; DeconstructFkConstraintRow(htup, &info->nkeys, info->conkey, diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 87002049538..2db011ddfb7 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo /* number of columns in the foreign key */ int nkeys; + /* Is enforced ? */ + bool conenforced; + /* * these arrays each have nkeys valid entries: */ diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 8c04a24b37d..36d3fdddaa5 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -1,12 +1,43 @@ -- -- FOREIGN KEY -- --- MATCH FULL +-- NOT ENFORCED -- -- First test, check and cascade -- CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text ); -CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int ); +CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED, + ftest2 int ); +-- Inserting into the foreign key table will not result in an error, even if +-- there is no matching key in the referenced table. +INSERT INTO FKTABLE VALUES (1, 2); +INSERT INTO FKTABLE VALUES (2, 3); +-- Check FKTABLE +SELECT * FROM FKTABLE; + ftest1 | ftest2 +--------+-------- + 1 | 2 + 2 | 3 +(2 rows) + +-- Changing it back to ENFORCED will result in an error due to validation of +-- existing rows. +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; +ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey" +DETAIL: Key (ftest1)=(1) is not present in table "pktable". +-- Remove any existing rows that violate the constraint, then attempt to change +-- it. +TRUNCATE FKTABLE; +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; +-- Any further inserts will fail due to the enforcement. +INSERT INTO FKTABLE VALUES (3, 4); +ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey" +DETAIL: Key (ftest1)=(3) is not present in table "pktable". +-- +-- MATCH FULL +-- +-- First test, check and cascade +-- -- Insert test data into PKTABLE INSERT INTO PKTABLE VALUES (1, 'Test1'); INSERT INTO PKTABLE VALUES (2, 'Test2'); @@ -351,6 +382,8 @@ INSERT INTO FKTABLE VALUES (1, NULL); ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL; ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey" DETAIL: MATCH FULL does not allow mixing of null and nonnull key values. +-- NOT ENFORCED will bypass the initial check. +ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED; DROP TABLE FKTABLE; DROP TABLE PKTABLE; -- MATCH SIMPLE @@ -1276,13 +1309,24 @@ INSERT INTO fktable VALUES (0, 20); ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey" DETAIL: Key (fk)=(20) is not present in table "pktable". COMMIT; +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED; +BEGIN; +-- doesn't match FK, no error. +UPDATE pktable SET id = 10 WHERE id = 5; +-- doesn't match PK, no error. +INSERT INTO fktable VALUES (0, 20); +ROLLBACK; -- try additional syntax -ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE; +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED; -- illegal option ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED; ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ... ^ +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED; +ERROR: conflicting constraint properties +LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC... + ^ -- test order of firing of FK triggers when several RI-induced changes need to -- be made to the same row. This was broken by subtransaction-related -- changes in 8.0. @@ -1580,7 +1624,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1; CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int); ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3; ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000); -ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk; +ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED; +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED; CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int); ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2; ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000); @@ -1665,6 +1710,37 @@ Indexes: Referenced by: TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) +-- Check the exsting FK trigger +CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint +FROM pg_trigger +WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass) + UNION ALL SELECT 'fk_notpartitioned_pk'::regclass); +SELECT count(1) FROM tmp_trg_info; + count +------- + 14 +(1 row) + +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED; +-- No triggers +SELECT count(1) FROM pg_trigger +WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass) + UNION ALL SELECT 'fk_notpartitioned_pk'::regclass); + count +------- + 0 +(1 row) + +-- Changing it back to ENFORCED will recreate the necessary triggers. +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED; +-- Should be exactly the same number of triggers found as before +SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt +ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint); + count +------- + 14 +(1 row) + ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey; -- done. DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk; @@ -1868,6 +1944,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502) Foreign-key constraints: TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE +DROP TABLE fk_partitioned_fk_2; +CREATE TABLE fk_partitioned_fk_2 (b int, a int, + FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED); +BEGIN; +ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502); +-- should have two constraints +\d fk_partitioned_fk_2 + Table "public.fk_partitioned_fk_2" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + b | integer | | | + a | integer | | | +Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502) +Foreign-key constraints: + "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED + TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE + +DROP TABLE fk_partitioned_fk_2; +ROLLBACK; +BEGIN; +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED; +ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502); +-- should have only one constraint +\d fk_partitioned_fk_2 + Table "public.fk_partitioned_fk_2" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + b | integer | | | + a | integer | | | +Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502) +Foreign-key constraints: + TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED + +ROLLBACK; DROP TABLE fk_partitioned_fk_2; CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a); CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100); diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index d1aac5357f0..96813331fc2 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -2,13 +2,39 @@ -- FOREIGN KEY -- --- MATCH FULL +-- NOT ENFORCED -- -- First test, check and cascade -- CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text ); -CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int ); +CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED, + ftest2 int ); +-- Inserting into the foreign key table will not result in an error, even if +-- there is no matching key in the referenced table. +INSERT INTO FKTABLE VALUES (1, 2); +INSERT INTO FKTABLE VALUES (2, 3); + +-- Check FKTABLE +SELECT * FROM FKTABLE; + +-- Changing it back to ENFORCED will result in an error due to validation of +-- existing rows. +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; + +-- Remove any existing rows that violate the constraint, then attempt to change +-- it. +TRUNCATE FKTABLE; +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; + +-- Any further inserts will fail due to the enforcement. +INSERT INTO FKTABLE VALUES (3, 4); + +-- +-- MATCH FULL +-- +-- First test, check and cascade +-- -- Insert test data into PKTABLE INSERT INTO PKTABLE VALUES (1, 'Test1'); INSERT INTO PKTABLE VALUES (2, 'Test2'); @@ -230,6 +256,9 @@ INSERT INTO FKTABLE VALUES (1, NULL); ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL; +-- NOT ENFORCED will bypass the initial check. +ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED; + DROP TABLE FKTABLE; DROP TABLE PKTABLE; @@ -968,10 +997,22 @@ INSERT INTO fktable VALUES (0, 20); COMMIT; +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED; + +BEGIN; + +-- doesn't match FK, no error. +UPDATE pktable SET id = 10 WHERE id = 5; +-- doesn't match PK, no error. +INSERT INTO fktable VALUES (0, 20); + +ROLLBACK; + -- try additional syntax -ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE; +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED; -- illegal option ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED; -- test order of firing of FK triggers when several RI-induced changes need to -- be made to the same row. This was broken by subtransaction-related @@ -1182,7 +1223,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1; CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int); ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3; ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000); -ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk; +ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED; +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED; CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int); ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2; ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000); @@ -1236,6 +1278,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500; UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500; -- check psql behavior \d fk_notpartitioned_pk + +-- Check the exsting FK trigger +CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint +FROM pg_trigger +WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass) + UNION ALL SELECT 'fk_notpartitioned_pk'::regclass); +SELECT count(1) FROM tmp_trg_info; + +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED; +-- No triggers +SELECT count(1) FROM pg_trigger +WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass) + UNION ALL SELECT 'fk_notpartitioned_pk'::regclass); + +-- Changing it back to ENFORCED will recreate the necessary triggers. +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED; + +-- Should be exactly the same number of triggers found as before +SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt +ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint); + ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey; -- done. DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk; @@ -1372,6 +1435,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN \d fk_partitioned_fk_2 DROP TABLE fk_partitioned_fk_2; +CREATE TABLE fk_partitioned_fk_2 (b int, a int, + FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED); +BEGIN; +ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502); +-- should have two constraints +\d fk_partitioned_fk_2 +DROP TABLE fk_partitioned_fk_2; +ROLLBACK; +BEGIN; +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED; +ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502); +-- should have only one constraint +\d fk_partitioned_fk_2 +ROLLBACK; +DROP TABLE fk_partitioned_fk_2; + CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a); CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100); CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL); -- 2.18.0 ^ permalink raw reply [nested|flat] 23+ messages in thread
end of thread, other threads:[~2024-10-08 09:06 UTC | newest] Thread overview: 23+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2020-12-31 12:20 [PATCH v1 2/2] WIP: buffer alloc specialized for relation extension Luc Vlaming <[email protected]> 2024-10-08 09:06 NOT ENFORCED constraint feature Amul Sul <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox