Description: I`m using mongoDB on my project. This is short logic for handler when user tries to put his item for sale. Before putting offer to mongo I validate the offer, so there would be no active offers with save assetId
Using:
mgo.v2
mongo 3.6
golang 1.10
Problem: If user clicks really fast sends several requests to my handler (lets say if he double click the mouse fast), validation doesn`t work as it seems like the first offer is not in the collection yet, so as a result I get 2-3 offers with same assetId.
I tried
Question:
Is there any way I can handle this with mongo instruments, or someone would suggest some other workaround?
Thank you in advance!
count, _ := r.DB.C(sellOfferCollectionName).Find(
bson.M{
"state": someState,
"asset_id": assetId,
"seller_id": seller,
},
).Count()
if count > 0 {
return
}
id := uuid.New().String()
OfferModel := Offer{
Id: id,
AssetId: assetId,
State: someState,
SellerId: sellerId,
CreatingDate: time.Now(),
}
if _, err := r.DB.C(sellOfferCollectionName).UpsertId(offer.Id, offer); err != nil {
return err
}
UPDATE
I tried to recreate the problem even more. So I wrote this little test code, so in result managed to write 60 documents before validation (count > 0) worked. This example fully recreates my problem.
type User struct {
Id string `bson:"_id"`
FirstName string `bson:"first_name"`
LastName string `bson:"last_name"`
State bool `bson:"state"`
}
func main() {
mongoSession, mgErr := mgo.Dial("127.0.0.1:27018")
if mgErr != nil {
panic(mgErr)
}
var mongoDbSession = mongoSession.DB("test_db")
for i := 0; i < 1000; i++ {
go func() {
count, _ := mongoDbSession.C("users").Find(
bson.M{
"state": true,
"first_name": "testUser",
},
).Count()
if count > 0 {
return
}
user := User{
Id: uuid.New().String(),
FirstName: "testUser",
LastName: "testLastName",
State: true,
}
if _, err := mongoDbSession.C("users").UpsertId(user.Id, user); err != nil {
panic(mgErr)
}
}()
}
count, _ := mongoDbSession.C("users").Find(
bson.M{
"state": true,
"first_name": "testUser",
},
).Count()
fmt.Println(count)
fmt.Scanln()
}
Eventually, after close investigation the bug, we found out that the reason was when user sent request it was handled in goroutine. Means a lot requests = a lot of concurrent goroutines. So, it out validator (check if the offer is in the collection), couldn't find it as it was not in the mongo yet. So, in the end, we decided to use redis as our validator.
Here is short implementation:
incr, err := redisClient.Incr(offer.AssetId).Result()
if err != nil {
return err
}
if incr > 1 {
return errors.New("ASSET_ALREADY_ON_SALE")
}
redisClient.Expire(offer.AssetId, time.Second*10)
Hope it will help someone facing same issue.
First thing would be to disable the "Send" button at client side while the call is in progress, so if the user double or triple clicks, that will have no effect, as the second and subsequent calls will target a disabled button, hence nothing will happen.
If the same order may come from multiple places which you want to save multiple times, this is already enough and the correct way to do it.
If the ID also comes from the client, and if only a single order may exist with the given ID, then the next thing you should do is simply use the Order ID as the document ID in MongoDB: assign and use this value as the MongoDB _id
field. This will give you the guarantee that multiple items with the same order ID will not exists, the 2nd attempt to insert the order would return an error. Note that using Query.UpsertId()
will always succeed, inserting the document if not exists, and updating it if it does. Query.Insert()
insert the document if it does not exists, and returns an error if it already does. Using none of UpsertId()
and Insert()
will result in multiple documents with the same ID.
If for some reason you can't or don't want to use the order ID as the document ID, then define a unique index for the property which stores the order ID, for details see MongoDB Unique Indexes.
Note that using the MongoDB _id
field or another one with unique index in itself ensures you can't insert multiple documents with the same Order ID (ensured by MongoDB). Also note that this will work even if you have a cluster with multiple MongoDB instances, as writes (including inserts) always happen at the Master node. So nothing else is required for this to work in a multi-server cluster environment.