AOSSIE-Org / AOSSIE-Org/DebateAI

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

Abierto
#475 2 comentarios 0 reacciones 1 asignado Reclamado por @Ri1tik Ver en GitHub
bug
Lenguaje dominante
TypeScript
Estrellas
84
Forks
198
Merge medio
2 d 19 h
PR fusionados (30 d)
30

Descripción

### 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

Guía de contribución

No hay ninguna guía de contribución indexada para este repositorio

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.