[5.x]: GC on PostgreSQL can leave all FK triggers disabled and deletes stop cascading
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 48/100
Hướng nghiên cứu
Start with src/services/Gc.php, especially _deleteOrphanedFkRows() and the checkIntegrity(false) call; compare its behavior with Yii's PostgreSQL QueryBuilder described in the issue. Reproduce on PostgreSQL by interrupting GC after triggers are disabled, then check pg_trigger for disabled triggers and verify cascading deletes. Done means GC cannot leave FK enforcement disabled after failure and does not disable it for other connections.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
What happened?
Description
On PostgreSQL, Gc::_deleteOrphanedFkRows() disables integrity checks with checkIntegrity(false) (https://github.com/craftcms/cms/blob/5.x/src/services/Gc.php#L684), which Yii's pgsql QueryBuilder builds as ALTER TABLE ... DISABLE TRIGGER ALL for every table (https://github.com/yiisoft/yii2/blob/master/framework/db/pgsql/QueryBuilder.php#L223). The change is a schema change, not a session setting, so it applies to every connection. Integrity checks are only turned back on by the last statement of the method, not in a finally.
This causes two problems:
- If GC dies between those two points, every FK trigger in the database stays disabled indefinitely. GC runs on web requests (
probability), so a request that times out, hits a memory limit or throws partway through leaves the database with no FK enforcement. Deletes stop cascading, and every element, draft, revision, structure or plugin row deleted afterwards leaves orphans in its child tables. - Even when GC completes, every other request's deletes skip cascades while the loop runs, because the triggers are disabled for all connections, not just GC's.
On MySQL this doesn't happen: SET FOREIGN_KEY_CHECKS = 0 only lasts for GC's own connection and ends with it.
We found a production site where all 736 FK triggers (184 FKs × 4) were disabled. It had built up orphans in 27 FK relationships: elements_sites, entries, elements_owners, structureelements, relations, changedfields, Neo blocks, Blitz cache tables and searchindexqueue_fields. Most of them appeared after September 2026. Nothing errored on the site itself; it only showed up when:
- a
pg_dumpbackup failed to restore 27 of its FKs (the dump's data violates constraints that the source database still defines), and - a plugin migration that reads
elements_sites.content(Hyper 3's relation rebuild) failed on content rows whose element no longer exists.
You can check whether a site is affected with SQL:
SELECT count(*) FROM pg_trigger WHERE tgenabled = 'D';
Or with a Craft CLI:
php craft exec "Craft::\$app->db->createCommand(\"SELECT count(*) FROM pg_trigger WHERE tgenabled = 'D'\")->queryScalar()"
The garbage clean code that introduced the behaviour was 80d5c39 and has been unchanged since 032d500.
Our workaround is to set 'gc' => ['probability' => 0] in config/app.php and run the craft gc via a cron job.
Steps to reproduce
- Use Craft 5 on PostgreSQL, with a DB user that owns the tables so
DISABLE TRIGGER ALLsucceeds. - Make GC fail after the disable step. For example, run
craft gcand kill the process during "deleting orphaned foreign key rows", or throw from inside the FK loop. - Run
SELECT count(*) FROM pg_trigger WHERE tgenabled = 'D';. It's non-zero (every FK trigger). - Delete an entry with nested entries or Neo blocks: its
elements_sites,entries,elements_ownersetc. rows are left behind.
Expected behavior
- An error or timeout during GC should never leave FK enforcement switched off for the whole database.
- GC shouldn't turn off FK enforcement for other connections at all.
Actual behavior
The foreign keys are all disabled and there are orphaned rows left in the database which can cause problems for plugin migrations that fetch the raw row data.
Suggestions
- Re-enable in a
finallyblock, so an exception can't skip it. That doesn't help if the PHP process is killed (e.g. a timeout), though. - Have GC or
craft db/repairwarn whenpg_triggerhas disabled RI triggers, so affected sites can find out.
Craft CMS version
5.11.5.1
PHP version
8.4
Operating system and version
Ubuntu 22.04
Database type and version
PostgreSQL 15.19
Image driver and version
No response
Installed plugins and versions
Neo, Blitz, Formie, Hyper, Navigation, SEOmatic, Retour (the orphans span core and plugin tables)
- Ngôn ngữ chính
- PHP
- Star
- 3.6k
- Fork
- 712
- Merge trung bình
- 13 giờ 8 phút
- Pull request đã merge (30 ngày)
- 366
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của craftcms/cms
-
bug repo:cms
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
craftcms/cms#20205 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[6.x]: Element index View menu: sort direction buttons only update after the table has loadedĐang mởbug repo:cms
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 64/100
craftcms/cms#20203 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[5.x] MCP: `configuration.update` fails for Matrix fields with TypeError on `$propagationMethod`Đang mởbug repo:cms
Độ khó 4/5 1-2 ngày Mức phù hợp với người mới 55/100
craftcms/cms#20406 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
repo:cms
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 73/100
craftcms/cms#20405 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[5.x]: MCP returns `[]` for CKEditor field valuesCó thể đã có người làm @i-just đã nhận hôm nay. Đang mởrepo:cms
craftcms/cms#20404 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
Issue tương tự
-
Administration: required plugin configuration field stays invalid after restoring its saved valueĐang mởneeds-triage Platform(Default)
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
Maintainer thường phản hồi trong vòng 1 ngày
-
good first issue priority:low type:docs
Độ khó 1/5 1-3 giờ Mức phù hợp với người mới 95/100
crazy-goat/rabbit-stream#969 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
opensourcepos/opensourcepos#4801 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
backend docs
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
Open-Locker/Open-Locker#368 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement priority Sales
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
novalo-dev/Prestashop-MCP-Server#190 ·
Maintainer thường phản hồi trong vòng 1 ngày