From bb93928099d13e5d78a83ef1f7c224af1fc409ae Mon Sep 17 00:00:00 2001 From: web-dev0521 Date: Fri, 8 May 2026 08:12:20 -0400 Subject: [PATCH] =?UTF-8?q?:bug:=20Fix=20lost-update=20race=20on=20team.fe?= =?UTF-8?q?atures=20during=20concurrent=20file=20cr=E2=80=A6=20(#9198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: Fix lost-update race on team.features during concurrent file creation * :books: Add CHANGES.md entry for team.features race condition fix (#9197) --- CHANGES.md | 1 + backend/src/app/rpc/commands/files_create.clj | 38 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 196ffa7153..edacca0551 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -73,6 +73,7 @@ ### :bug: Bugs fixed +- Fix lost-update race on `team.features` during concurrent file creation: two simultaneous create-file requests on the same team could both read the same features snapshot, compute different unions, and have the second `UPDATE` silently overwrite the first; the write is now preceded by a `SELECT … FOR UPDATE` inside the same transaction so every update sees the latest committed state [Github #9197](https://github.com/penpot/penpot/issues/9197) - Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361) - Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) - Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627) diff --git a/backend/src/app/rpc/commands/files_create.clj b/backend/src/app/rpc/commands/files_create.clj index a0d1d9d516..c83b5acf83 100644 --- a/backend/src/app/rpc/commands/files_create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -112,22 +112,30 @@ ::quotes/profile-id profile-id ::quotes/project-id project-id}) - ;; FIXME: IMPORTANT: this code can have race conditions, because - ;; we have no locks for updating team so, creating two files - ;; concurrently can lead to lost team features updating - (when-let [features (-> features - (set/difference (:features team)) - (set/difference cfeat/no-team-inheritable-features) - (not-empty))] - (let [features (-> features - (set/union (:features team)) - (set/difference cfeat/no-team-inheritable-features) - (into-array))] + ;; Acquire a row-level lock on the team and re-read its features + ;; inside the same transaction before the read-modify-write below. + ;; Without the lock, two concurrent create-file calls on the same + ;; team can both observe the same team.features value, each + ;; compute a different union, and the second UPDATE silently + ;; overwrites the first (lost update under READ COMMITTED). + (let [team-features (-> (db/exec-one! conn + ["SELECT features FROM team WHERE id = ? FOR UPDATE" + team-id]) + :features + (db/decode-pgarray #{}))] + (when-let [new-features (-> features + (set/difference team-features) + (set/difference cfeat/no-team-inheritable-features) + (not-empty))] + (let [features (-> new-features + (set/union team-features) + (set/difference cfeat/no-team-inheritable-features) + (into-array))] - (db/update! conn :team - {:features features} - {:id (:id team)} - {::db/return-keys false}))) + (db/update! conn :team + {:features features} + {:id team-id} + {::db/return-keys false})))) (-> (create-file cfg params) (vary-meta assoc ::audit/props {:team-id team-id}))))