Cloud Code Concurrency Issue

Hello folks,

I have 2 Parse Servers hosting the same application on production (500+ requests/second). Requests are distributed using a load balancer. I have encountered an issue with Parse Cloud Code.

Minimum reproducible code of the problem.

Parse.Cloud.afterSave("Shop", async (request) => {
    // Ad Creation
    const pShop = request.object; // Parse Object 'Shop'
    const shopName = pShop.get('name'); // Name of the Shop
    const shopIdentifier = `"${pShop.id}.${shopName}"`; // Formatted for logging

    // Check for any existing advertisements
    const query = new Parse.Query('Advertisement');
    query.equalTo('shop', pShop); // 'shop' field is of type Shop Pointer
    query.equalTo('active', true); // Only active ads are searched

    const result = await query.first();
    if (result === undefined) {
        // No ads exists, create a new one.
        console.log(`Ad not found. Creating one for ${shopIdentifier}`);
        const pAd = new Parse.Object('Advertisement');
        pAd.set('shop', pShop);
        pAd.set('active', true);

        // Random title set for the reproducible code, production may have different titles based on modification.
        pAd.set('title', `Random Title #${Math.floor(Math.random() * 100)}`);
        await pAd.save();
        /*
         * PROBLEM: When the Shop is modified concurrently by multiple users (> 100) at the same time,
         *
         * This block is executed multiple times which ends up
         * creating a lot of duplicate Ads for the same shop.
         */
    } else {
        console.log(`An Advertisement already exists for ${shopIdentifier}`);
    }
});

Some kind of central locking system could possibly solve this. I would like to know the thoughts of the community as to how to overcome this problem in Cloud Code. Thank You.

You can create a unique index for shop field for Advertisement collection. Seems like you want each shop to have only 1 ad. Unique index can do that. Ä°f another parse server create an ad for same shop, database will throw an error.

That’s a good idea. Apologies! My reproducible code is not very accurate. I can have multiple ad entries for the same shop with different settings of the ad like title, images, etc… The problem is duplicate creation.

PS: I’ve modified the source code to include such details.

Well. Idea is still valid. I would create a unique key based on your requirements and let the database handle the concurrency issues.

1 Like

Thanks for replying. There are other scenarios I wish to consider. If the Ad is modified instead. What can happen? There are multiple writes to the same Parse Object. My understanding is very limited. My prediction is that the last write would be the final modification. If it’s a business-critical logic, I want to force lock this object to prevent further modification until the previous one is complete. It’s because the next modification would depend on the previous values of the same Parse Object.

In 95% of cases, this is not required. I was considering to re-work the logic. But then I realized it’s vital to lock Parse Objects for some administrative use cases.

Hi @chillaxdev , as i understand, you need to ensure to create by default 1 Ad by Shop if the shop do not have already an Ad.

A solution coud be to use an atomic function like findOneAndUpdate (with upsert) with a mongo client. But it will bypass parse server hooks, it need advanced knowledge, so not a good solution.

@uzaysan has a good idea but may be incomplete.

In your use case unique index could do the job: it seems that you need to create a “default” Ad. In this case you need to identify this kind of Ad in you DB. Then unique index could solve your issue.

  • add a String field “defaultAdShopId” on your Ad class
  • add a unique index on this field
  • in your shop afterSave, you have the shop objectId, when you create the Ad, concat the shopId with for example the “default” word, example : const uniqueAdIdentifier = shop.id + "default"
  • pAd.set("defaultAdShopId", uniqueAdIdentifier)
  • then save

And you are done, the save will fail if you already have the default Ad for a shop in you Ad Class.

Not tested but in theory it should work

1 Like

If you’ll only have one active ad per shop, I’d say the unique index is the way to go. However, if you’ll have more than one, I would go for creating an AdCreationQueue class to centralize ad creation in a cloud job. Something like this…

In the Shop trigger:

Parse.Cloud.beforeSave('Shop', async (request) => {
  ...
  const queuedRequest = await new Parse.Object('AdCreationQueue').save({ shop }); 
})

Then process it:

// cloud job or function
Parse.Cloud.job('adCreationQueue:processNext', async (request) => {

  const nextReq = await new Parse.Query('AdCreationQueue').first();
  if (!nextReq) return;

  const shopId = nextReq.shopId;
  await checkIfHasAdOrCreateOne(shopId);

  const similars = await new Parse.Query('AdCreationQueue')
    .equalTo('shopId', shopId)
    .find();

  await Parse.Objejct.destroyAll(similars);   // Clean all requests for this Shop
})
1 Like