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 件 担当者 1 名 @Ri1tik が担当を希望しています GitHub で見る
bug
主要言語
TypeScript
スター
84
フォーク
198
平均マージ
2日 19時間
マージ済み PR(30日)
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 を短くまとめたダイジェスト。