Skip to content

Commit af3f948

Browse files
committed
[yugabyte#25431] YSQL: Perform read for deferred FK trigger at txn end
Summary: FK constraint has the following optimization for performing batched read: - FK constraint registers ybctids which need to be checked - on checking particular ybctid for particular FK constraint all registered ybctds are read at once, result is cached - checking ybctid for another FK constraint will check cached result instead of making real read request. In Postgres constraint could be of 2 types: - IMMEDIATE (default). Checked at statement end. - DEFERRED. Checked at transaction end. Nowadays FK optimization work incorrectly in case multiple constraint of different types is used in same transaction. And the reason is that YSQL registers ybctids for both types of constraint in single map. Example: ``` 1. CREATE TABLE pk_t(k INT PRIMARY KEY); 2. CREATE TABLE fk_t(k INT PRIMARY KEY, pk_1 INT REFERENCES pk_t(k), pk_2 INT REFERENCES pk_t(k) DEFERRABLE INITIALLY DEFERRED); 3. INSERT INTO pk_t VALUES (1); 4. BEGIN; 5. INSERT INTO fk_t VALUES(1, 1, 2); 6. INSERT INTO pk_t VALUES(2); 7. COMMIT; ``` - On step #5 YSQL inserts value `(1, 1, 2)` into table with 2 FK referenced columns. Where constraint for second column is `DEFERRED`. - Both constraint registers ybctid for rows `k = 1` and `k = 2` in table `pk_t`. - Because constraint for first column is non deferred is it executed immediately (at the end of the statement). - Due to optimization both registered ybctids will be read at once. And result will be cached. And the result contains `k = 1` only, because `k = 2` is only inserted on step #6 - On step #7 YSQL will perform the check of constraint for second column and cached result will be used which doesn't have `k = 2` inserted on step #6 Solution is to store ybctids for deferred and non-deferred constraint in different structure. And read them independently. All the ybctids registered for deferred constraints will be read only on transaction commit step (step #7). For this purpose the new `YBCNotifyDeferredTriggersProcessingStarted()` function is introduced. Which is called straight before deferred triggers firing at the beginning of `COMMIT` command processing. Jira: DB-14665 Test Plan: Jenkins New unit test are introduced ``` ./yb_build.sh --gtest_filter PgFKeyTest.DeferredConstraintReadAtTxnEnd ``` Reviewers: pjain, myang, kramanathan, patnaik.balivada Reviewed By: pjain Subscribers: yql Tags: #jenkins-ready Differential Revision: https://phorge.dev.yugabyte.com/D40896
1 parent fc942c1 commit af3f948

File tree

13 files changed

+132
-51
lines changed

13 files changed

+132
-51
lines changed

src/postgres/src/backend/access/transam/xact.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,6 +2367,9 @@ CommitTransaction(void)
23672367
TransStateAsString(s->state));
23682368
Assert(s->parent == NULL);
23692369

2370+
if (IsYugaByteEnabled())
2371+
YBCNotifyDeferredTriggersProcessingStarted();
2372+
23702373
/*
23712374
* Do pre-commit processing that involves calling user-defined code, such
23722375
* as triggers. SECURITY_RESTRICTED_OPERATION contexts must not queue an

src/postgres/src/backend/commands/tablecmds.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12374,7 +12374,9 @@ YbGetNext(YbFKTriggerScanDesc desc, TupleTableSlot *slot)
1237412374
ExecDropSingleTupleTableSlot(new_slot);
1237512375
break;
1237612376
}
12377-
YbAddTriggerFKReferenceIntent(desc->trigger, desc->fk_rel, new_slot, desc->estate);
12377+
YbAddTriggerFKReferenceIntent(desc->trigger,
12378+
desc->fk_rel, new_slot, desc->estate,
12379+
/* is_deferred= */ false);
1237812380
desc->buffered_tuples[desc->buffered_tuples_size++] = new_slot;
1237912381
}
1238012382
}

src/postgres/src/backend/commands/trigger.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6620,8 +6620,13 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
66206620
}
66216621
}
66226622

6623-
if (IsYBBackedRelation(rel) && RI_FKey_trigger_type(trigger->tgfoid) == RI_TRIGGER_FK)
6624-
YbAddTriggerFKReferenceIntent(trigger, rel, newslot, estate);
6623+
if (IsYBBackedRelation(rel) &&
6624+
RI_FKey_trigger_type(trigger->tgfoid) == RI_TRIGGER_FK)
6625+
{
6626+
const bool is_deferred = new_shared.ybc_txn_fdw_tuplestore != NULL;
6627+
YbAddTriggerFKReferenceIntent(trigger, rel, newslot, estate,
6628+
is_deferred);
6629+
}
66256630

66266631
afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth].events,
66276632
&new_event, &new_shared);

src/postgres/src/backend/utils/adt/ri_triggers.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3375,7 +3375,8 @@ RI_FKey_trigger_type(Oid tgfoid)
33753375

33763376
void
33773377
YbAddTriggerFKReferenceIntent(Trigger *trigger, Relation fk_rel,
3378-
TupleTableSlot *new_slot, EState *estate)
3378+
TupleTableSlot *new_slot, EState *estate,
3379+
bool is_deferred)
33793380
{
33803381
YbcPgYBTupleIdDescriptor *descr;
33813382

@@ -3402,7 +3403,9 @@ YbAddTriggerFKReferenceIntent(Trigger *trigger, Relation fk_rel,
34023403
null_found = attr->is_null && (attr->attr_num > 0);
34033404

34043405
if (!null_found)
3405-
HandleYBStatus(YBCAddForeignKeyReferenceIntent(descr, YBCIsRegionLocal(fk_rel)));
3406+
HandleYBStatus(YBCAddForeignKeyReferenceIntent(descr,
3407+
YBCIsRegionLocal(fk_rel),
3408+
is_deferred));
34063409
pfree(descr);
34073410
}
34083411
}

src/postgres/src/include/commands/trigger.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ extern void RI_PartitionRemove_Check(Trigger *trigger, Relation fk_rel,
283283
Relation pk_rel);
284284
extern void YbAddTriggerFKReferenceIntent(Trigger *trigger, Relation fk_rel,
285285
TupleTableSlot *new_slot,
286-
EState *estate);
286+
EState *estate, bool is_deferred);
287287

288288
/* result values for RI_FKey_trigger_type: */
289289
#define RI_TRIGGER_PK 1 /* is a trigger on the PK relation */

src/postgres/src/test/regress/expected/yb.orig.foreign_key.out

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ INSERT INTO c VALUES(1, 1, 1, 1, 1);
618618
Trigger for constraint c_p2_fk_fkey: calls=1
619619
Trigger for constraint c_p4_fk_fkey: calls=1
620620
Storage Read Requests: 1
621-
Storage Rows Scanned: 4
621+
Storage Rows Scanned: 2
622622
Storage Write Requests: 1
623623
Storage Flush Requests: 1
624624
(9 rows)
@@ -699,7 +699,7 @@ INSERT INTO fk VALUES (500, 1); -- should fail
699699
ERROR: insert or update on table "fk" violates foreign key constraint "fk_id_name_fkey"
700700
DETAIL: Key (id, name)=(500, 1) is not present in table "pk".
701701
SELECT * from fk;
702-
id | name
702+
id | name
703703
----+------
704704
1 | 500
705705
(1 row)
@@ -721,7 +721,7 @@ INSERT INTO fk VALUES (200); -- should fail
721721
ERROR: insert or update on table "fk" violates foreign key constraint "fk_id_fkey"
722722
DETAIL: Key (id)=(200) is not present in table "pk".
723723
SELECT * FROM fk;
724-
id
724+
id
725725
-----
726726
1
727727
105
@@ -743,7 +743,7 @@ INSERT INTO fk VALUES (200); -- should fail
743743
ERROR: insert or update on table "fk" violates foreign key constraint "fk_b_fkey"
744744
DETAIL: Key (b)=(200) is not present in table "pk".
745745
SELECT * FROM fk;
746-
b
746+
b
747747
-----
748748
1
749749
105
@@ -764,7 +764,7 @@ INSERT INTO fk VALUES (150, 20); -- should fail
764764
ERROR: insert or update on table "fk" violates foreign key constraint "fk_a_c_fkey"
765765
DETAIL: Key (a, c)=(150, 20) is not present in table "pk".
766766
SELECT * FROM fk;
767-
a | c
767+
a | c
768768
---+----
769769
1 | 20
770770
(1 row)
@@ -787,7 +787,7 @@ INSERT INTO fk(a, c) VALUES (150, 20); -- should fail
787787
ERROR: insert or update on table "fk" violates foreign key constraint "fk_a_c_fkey"
788788
DETAIL: Key (a, c)=(150, 20) is not present in table "pk".
789789
SELECT * FROM fk;
790-
b | d | a | c
790+
b | d | a | c
791791
---+---+---+----
792792
| | 1 | 20
793793
(1 row)
@@ -810,7 +810,7 @@ DETAIL: Partition key of the failing row contains (a) = (20).
810810
INSERT into pk values (5);
811811
INSERT into fk values (5);
812812
SELECT * FROM fk;
813-
a
813+
a
814814
---
815815
5
816816
(1 row)
@@ -834,7 +834,7 @@ INSERT INTO fk VALUES (1, 10); -- should fail
834834
ERROR: insert or update on table "fk" violates foreign key constraint "fk_b2_fkey"
835835
DETAIL: Key (b2)=(10) is not present in table "pk2".
836836
SELECT * from fk;
837-
b | b2
837+
b | b2
838838
---+----
839839
1 | 1
840840
(1 row)
@@ -851,7 +851,7 @@ DETAIL: Key (a, b)=(2, 1) is not present in table "pk".
851851
INSERT INTO pk VALUES (5, 1);
852852
UPDATE fk set a = a + 4; -- should pass
853853
SELECT * from fk;
854-
a | b
854+
a | b
855855
---+---
856856
5 | 1
857857
(1 row)

src/yb/yql/pggate/pg_fk_reference_cache.cc

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ class PgFKReferenceCache::Impl {
4747

4848
void Clear() {
4949
references_.clear();
50-
intents_.clear();
50+
regular_intents_.clear();
51+
deferred_intents_.clear();
5152
region_local_tables_.clear();
53+
intents_ = &regular_intents_;
5254
}
5355

5456
void DeleteReference(const LightweightTableYbctid& key) {
@@ -66,26 +68,31 @@ class PgFKReferenceCache::Impl {
6668
return true;
6769
}
6870

71+
auto reader = reader_provider_();
72+
auto residual_capacity = std::min<size_t>(intents_->size(), buffering_settings_.max_batch_size);
73+
reader.Reserve(residual_capacity);
74+
6975
// Check existence of required FK intent.
70-
// Absence means the key was checked by previous batched request and was not found.
71-
// We don't need to call the reader in this case.
72-
auto it = intents_.find(key);
73-
if (it == intents_.end()) {
74-
return false;
76+
const auto intents_end = intents_->end();
77+
auto it = intents_->find(key);
78+
if (it == intents_end) {
79+
if (!IsDeferredTriggersProcessingStarted()) {
80+
// In case of processing non deferred intents absence means the key was checked by previous
81+
// batched request and was not found. We don't need to call the reader in this case.
82+
return false;
83+
}
84+
// In case of processing deferred intents absence of intent could be caused by
85+
// subtxnransaction rollback. In this case we have to make a read attempt.
86+
reader.Add(TableYbctid{key.table_id, std::string{key.ybctid}});
87+
} else {
88+
// If the reader fails to get the result, we fail the whole operation (and transaction).
89+
// Hence it's ok to extract (erase) the keys from intent before calling reader.
90+
reader.Add(std::move(intents_->extract(it).value()));
7591
}
92+
--residual_capacity;
7693

77-
auto reader = reader_provider_();
78-
auto available_capacity = std::min<size_t>(
79-
intents_.size(), buffering_settings_.max_batch_size);
80-
reader.Reserve(available_capacity);
81-
// If the reader fails to get the result, we fail the whole operation (and transaction).
82-
// Hence it's ok to extract (erase) the keys from intent before calling reader.
83-
reader.Add(std::move(intents_.extract(it).value()));
84-
--available_capacity;
85-
86-
for (auto it = intents_.begin();
87-
it != intents_.end() && available_capacity; --available_capacity) {
88-
reader.Add(std::move(intents_.extract(it++).value()));
94+
for (auto it = intents_->begin(); it != intents_end && residual_capacity; --residual_capacity) {
95+
reader.Add(std::move(intents_->extract(it++).value()));
8996
}
9097

9198
// Add the keys found in docdb to the FK cache.
@@ -98,23 +105,42 @@ class PgFKReferenceCache::Impl {
98105
return references_.contains(key);
99106
}
100107

101-
void AddIntent(const LightweightTableYbctid& key, bool is_region_local) {
108+
void AddIntent(const LightweightTableYbctid& key, const IntentOptions& options) {
109+
LOG_IF(DFATAL, IsDeferredTriggersProcessingStarted())
110+
<< "AddIntent is not expected after deferred trigger processing start";
111+
102112
if (references_.contains(key)) {
103113
return;
104114
}
105115

106-
if (is_region_local) {
116+
if (options.is_region_local) {
107117
region_local_tables_.insert(key.table_id);
108118
}
109-
DCHECK(is_region_local || !region_local_tables_.contains(key.table_id));
110-
intents_.emplace(key.table_id, std::string(key.ybctid));
119+
LOG_IF(DFATAL, !options.is_region_local && region_local_tables_.contains(key.table_id))
120+
<< "The " << key.table_id << " table was previously reported as region local";
121+
(options.is_deferred ? &deferred_intents_ : &regular_intents_)->emplace(
122+
key.table_id, std::string(key.ybctid));
123+
}
124+
125+
void OnDeferredTriggersProcessingStarted() {
126+
LOG_IF(DFATAL, IsDeferredTriggersProcessingStarted())
127+
<< "Multiple call of OnDeferredTriggersProcessingStarted is not expected";
128+
LOG_IF(DFATAL, !regular_intents_.empty())
129+
<< "OnDeferredTriggersProcessingStarted implies all non deferred intents were processed";
130+
intents_ = &deferred_intents_;
111131
}
112132

113133
private:
134+
[[nodiscard]] bool IsDeferredTriggersProcessingStarted() const {
135+
return intents_ == &deferred_intents_;
136+
}
137+
114138
YbctidReaderProvider& reader_provider_;
115139
const BufferingSettings& buffering_settings_;
116140
MemoryOptimizedTableYbctidSet references_;
117-
TableYbctidSet intents_;
141+
TableYbctidSet regular_intents_;
142+
TableYbctidSet deferred_intents_;
143+
TableYbctidSet* intents_ = &regular_intents_;
118144
OidSet region_local_tables_;
119145
};
120146

@@ -142,8 +168,13 @@ Result<bool> PgFKReferenceCache::IsReferenceExists(
142168
return impl_->IsReferenceExists(database_id, key);
143169
}
144170

145-
void PgFKReferenceCache::AddIntent(const LightweightTableYbctid& key, bool is_region_local) {
146-
impl_->AddIntent(key, is_region_local);
171+
void PgFKReferenceCache::AddIntent(
172+
const LightweightTableYbctid& key, const IntentOptions& options) {
173+
impl_->AddIntent(key, options);
174+
}
175+
176+
void PgFKReferenceCache::OnDeferredTriggersProcessingStarted() {
177+
impl_->OnDeferredTriggersProcessingStarted();
147178
}
148179

149180
} // namespace yb::pggate

src/yb/yql/pggate/pg_fk_reference_cache.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ struct BufferingSettings;
2828

2929
class PgFKReferenceCache {
3030
public:
31+
struct IntentOptions {
32+
bool is_region_local;
33+
bool is_deferred;
34+
};
35+
3136
PgFKReferenceCache(YbctidReaderProvider& reader_provider,
3237
std::reference_wrapper<const BufferingSettings> buffering_settings);
3338
~PgFKReferenceCache();
@@ -36,7 +41,8 @@ class PgFKReferenceCache {
3641
void DeleteReference(const LightweightTableYbctid& key);
3742
void AddReference(const LightweightTableYbctid& key);
3843
Result<bool> IsReferenceExists(PgOid database_id, const LightweightTableYbctid& key);
39-
void AddIntent(const LightweightTableYbctid& key, bool is_region_local);
44+
void AddIntent(const LightweightTableYbctid& key, const IntentOptions& options);
45+
void OnDeferredTriggersProcessingStarted();
4046

4147
private:
4248
class Impl;

src/yb/yql/pggate/pggate.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1968,8 +1968,8 @@ Result<bool> PgApiImpl::ForeignKeyReferenceExists(
19681968
}
19691969

19701970
void PgApiImpl::AddForeignKeyReferenceIntent(
1971-
PgOid table_id, bool is_region_local, const Slice& ybctid) {
1972-
fk_reference_cache_.AddIntent(LightweightTableYbctid{table_id, ybctid}, is_region_local);
1971+
PgOid table_id, const Slice& ybctid, const PgFKReferenceCache::IntentOptions& options) {
1972+
fk_reference_cache_.AddIntent(LightweightTableYbctid{table_id, ybctid}, options);
19731973
}
19741974

19751975
void PgApiImpl::DeleteForeignKeyReference(PgOid table_id, const Slice& ybctid) {
@@ -1980,6 +1980,10 @@ void PgApiImpl::AddForeignKeyReference(PgOid table_id, const Slice& ybctid) {
19801980
fk_reference_cache_.AddReference(LightweightTableYbctid{table_id, ybctid});
19811981
}
19821982

1983+
void PgApiImpl::NotifyDeferredTriggersProcessingStarted() {
1984+
fk_reference_cache_.OnDeferredTriggersProcessingStarted();
1985+
}
1986+
19831987
Status PgApiImpl::AddExplicitRowLockIntent(
19841988
const PgObjectId& table_id, const Slice& ybctid, const YbcPgExplicitRowLockParams& params,
19851989
bool is_region_local, YbcPgExplicitRowLockErrorInfo& error_info) {

src/yb/yql/pggate/pggate.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,9 @@ class PgApiImpl {
704704
void DeleteForeignKeyReference(PgOid table_id, const Slice& ybctid);
705705
void AddForeignKeyReference(PgOid table_id, const Slice& ybctid);
706706
Result<bool> ForeignKeyReferenceExists(PgOid table_id, const Slice& ybctid, PgOid database_id);
707-
void AddForeignKeyReferenceIntent(PgOid table_id, bool is_region_local, const Slice& ybctid);
707+
void AddForeignKeyReferenceIntent(
708+
PgOid table_id, const Slice& ybctid, const PgFKReferenceCache::IntentOptions& options);
709+
void NotifyDeferredTriggersProcessingStarted();
708710

709711
Status AddExplicitRowLockIntent(
710712
const PgObjectId& table_id, const Slice& ybctid,

0 commit comments

Comments
 (0)