Skip to content

[BUG]: Unlike path can decrement like count multiple times per like — count drifts and can go negative #475

Description

@RounakKumarAgarwal

Bug Description

In ToggleLikeHandler (backend/controllers/like_controller.go), the like and unlike paths are asymmetric in how they guard the count update, and the unlike path has a race condition the like path doesn't.

Like path (race-safe):

set, err := db.RedisClient.SetNX(ctx, userKey, "1", 0).Result()
...
if set {   // only increment if THIS request actually created the key
    db.RedisClient.ZIncrBy(ctx, key, 1, postID)
    postCollection.UpdateOne(ctx, bson.M{"_id": postObjectID}, bson.M{"$inc": bson.M{"likeCount": 1}})
}

SetNX is atomic and set is true only if the key didn't already exist, so concurrent likes can't double-count. (The code even comments that this prevents the race.)

Unlike path (NOT race-safe):

_, err = db.RedisClient.Del(ctx, userKey).Result()   // deleted-count discarded
if err == nil {                                       // Del returning 0 is not an error
    db.RedisClient.ZIncrBy(ctx, key, -1, postID)
    postCollection.UpdateOne(ctx, bson.M{"_id": postObjectID}, bson.M{"$inc": bson.M{"likeCount": -1}})
}

The deleted-count from Del is discarded (_), and the decrement runs on err == nil alone. Del returning 0 (key already gone) is not an error, so it still passes the check.

Race: two concurrent unlike requests for the same post/user both pass the earlier Exists check, both call Del, but only one actually removes the key. Both still see err == nil, so both decrement likeCount (and the Redis ZSet). A single like gets decremented twice — the count drifts and can go negative.

Steps to Reproduce

  1. Like a post (likeCount = 1, user like key exists in Redis).
  2. Fire two unlike requests for the same post/user nearly simultaneously (double-tap, retry, or two tabs).
  3. Both pass the Exists check and both run the -1 decrement.
  4. Observe likeCount ends at -1 instead of 0 (drifts negative / desyncs from actual like state).

Logs and Screenshots

Unlike path discards Del's deleted-count (like_controller.go ~line 62):

_, err = db.RedisClient.Del(ctx, userKey).Result()
if err == nil {
    // decrement runs even if 0 keys were deleted
}

Suggested fix — gate on the deleted count, symmetric with the like path's if set:

deleted, err := db.RedisClient.Del(ctx, userKey).Result()
if err == nil && deleted > 0 {
    db.RedisClient.ZIncrBy(ctx, key, -1, postID)
    postCollection.UpdateOne(ctx, bson.M{"_id": postObjectID}, bson.M{"$inc": bson.M{"likeCount": -1}})
}

Environment Details

  • File: backend/controllers/like_controller.go (ToggleLikeHandler, unlike branch)
  • Backend: Go / Redis / MongoDB
  • Branch: main
  • Note: surfaces under concurrent or duplicate unlike requests (double-tap, retries, multiple tabs); the like path already guards against this via SetNX + if set, the unlike path does not

Impact

Low - Minor inconvenience

Code of Conduct

  • I have joined the Discord server and will post updates there
  • I have searched existing issues to avoid duplicates

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions