AOSSIE-Org / AOSSIE-Org/DebateAI

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

未关闭
#475 2 条评论 0 个 reaction 已指派 1 人 已被 @Ri1tik 认领 在 GitHub 查看
bug
主要语言
TypeScript
星标
84
派生
198
平均合并
2 天 19 小时
30 天内合并 PR
30

描述

### 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):**
```go
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):**
```go
_, 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

- [x] I have joined the [Discord server](https://discord.gg/hjUhu33uAn) and will post updates there
- [x] I have searched existing issues to avoid duplicates

贡献指南

这个仓库没有索引到贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。