Skip to content

Double decrement on counter_cache under specific conditions #32079

@malikolivier

Description

@malikolivier

Steps to reproduce

The following gist contains a failing unit test:
https://gist.github.com/malikolivier/87904c9ed078dca186cf455a09a4f30e

Expected behavior

Test should succeed, counter_cache should be decremented once.

Actual behavior

Test fails, counter_cache is decremented twice.

System configuration

Rails version: 5.1.5 (master)

Ruby version: 2.4.3

More details

Database setup: A lecture is given by a person in an institution. An institution can hold several lectures and a person can give several lectures.
Each person and institution has a cache counter counting the number of lectures associated.

Run the file included in the gist:

ruby decrement_counter_cache_test.rb

This file moves a lecture to another institution. We thus except the counter showing the number of lectures in the original institution to be decremented.

You will get the following output:

D, [2018-02-22T19:01:14.445281 #20237] DEBUG -- :   Institution Load (0.0ms)  SELECT  "institutions".* FROM "institutions" WHERE "institutions"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
D, [2018-02-22T19:01:14.446164 #20237] DEBUG -- :   Institution Update All (0.1ms)  UPDATE "institutions" SET "lectures_count" = COALESCE("lectures_count", 0) + 1 WHERE "institutions"."id" IS NULL
D, [2018-02-22T19:01:14.446526 #20237] DEBUG -- :   Institution Update All (0.0ms)  UPDATE "institutions" SET "lectures_count" = COALESCE("lectures_count", 0) - 1 WHERE "institutions"."id" = ?  [["id", 1]]  <- First decrement
D, [2018-02-22T19:01:14.446999 #20237] DEBUG -- :   Institution Create (0.0ms)  INSERT INTO "institutions" ("address") VALUES (?)  [["address", "New address"]]
D, [2018-02-22T19:01:14.447429 #20237] DEBUG -- :   Lecture Update (0.0ms)  UPDATE "lectures" SET "institution_id" = ? WHERE "lectures"."id" = ?  [["institution_id", 2], ["id", 1]]
D, [2018-02-22T19:01:14.447822 #20237] DEBUG -- :   Institution Update All (0.0ms)  UPDATE "institutions" SET "lectures_count" = COALESCE("lectures_count", 0) + 1 WHERE "institutions"."id" = ?  [["id", 2]]
D, [2018-02-22T19:01:14.448094 #20237] DEBUG -- :   Institution Update All (0.0ms)  UPDATE "institutions" SET "lectures_count" = COALESCE("lectures_count", 0) - 1 WHERE "institutions"."id" = ?  [["id", 1]]  <- Second decrement
D, [2018-02-22T19:01:14.448218 #20237] DEBUG -- :    (0.0ms)  commit transaction

As you can see, institutions.lectures_count is decremented twice. Now very interestingly, if you change this line to

belongs_to :person#, counter_cache: true

the unit test will succeed. The second decrement will not occur.

Conclusion: It seems that the creation of a second cache counter to another record interferes with the the way other cache counters are computed, causing a superfluous decrement. This happens while updating a record's attributes in bulk. A quick survey seemed to show that assign_attributes decrements the counter once, and save! decrements it a second time in the following code: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/persistence.rb#L429-L430.

May or may not be related to #31491 and #31493.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions