Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to distribute unique coupon codes with 70 requests/sec using Node.js

I run a coupon site that sees 50-70 requests/sec when we launch our deals (we launch 20+ deals at once multiple times a day). When the deals go live our users press a button to claim a coupon for a specific product, which serves up a unique coupon code via an ajax https request. Each coupon can only be redeemed once.

My issue is that with such high traffic volume at these times, the same coupon can be distributed to multiple users. This is bad as only one of them will be able to actually redeem the coupon leaving for a poor user experience for the other.

I am storing all the coupon info in objects on memory on a node.js server hosted by IBM Bluemix. I figured this would allow me to handle the requests quickly.

How I store the coupon info:

global.coupons = {};

//the number of coupons given for each product
global.given = {};

/*   Setting the coupon information */

//....I query my database for the products to be given today 

for(var i = 0; i < results.length; i++){
     var product = results[i];

     //add only the coupons to give today to the array
     var originalCoups = product.get('coupons');
     var numToTake = product.get('toGivePerDay');

      if(product.get('givenToday') > 0){
           numToTake = numToTake - product.get('givenToday');
      }
      // Example coupon array [["VVXM-Q577J2-XRGHCC","VVLE-JJR364-5G5Q6B"]]
      var couponArray = originalCoups[0].splice(product.get('given'), numToTake);

      //set promo info
      global.coupons[product.id] = couponArray;
      global.given[product.id] = 0;
}

Handle Coupon Request:

app.post('/getCoupon', urlencodedParser, function(req, res){
   if (!req.body) return res.status(400).send("Bad Request");
   if (!req.body.category) return res.status(200).send("Please Refresh the Page.");

        //Go grab a coupon
        var coupon = getUserACoupon(req.body.objectId);

        res.type('text/plain');
        res.status(200).send(coupon);

        if(coupon != "Sold Out!" && coupon != "Bad Request: Object does not exist."){

            //Update user & product analytics
            setStatsAfterCouponsSent(req.body.objectId, req.body.sellerProduct, req.body.userEmail, req.body.purchaseProfileId, coupon, req.body.category);

        }
});

//getCoupon logic
function getUserACoupon(objectId){

    var coupToReturn;

    // coupon array for the requseted product
    var coupsArray = global.coupons[objectId];

    if(typeof coupsArray != 'undefined'){

        // grab the number of coupons already given for this product and increase by one
        var num = global.given[objectId]; 
        global.given[objectId] = num+1;

        if(num < coupsArray.length){
            if(coupsArray[num] != '' && typeof coupsArray[num] != 'undefined' && coupsArray[num] != 'undefined'){

                coupToReturn = coupsArray[num];

            }else{
                console.log("Error with the coupon for "+objectId + " the num is " + num);
                coupToReturn = "Sold Out!";
                wasSoldOut(objectId);
            }
        }else{
            console.log("Sold out "+objectId+" with num " + num);
            coupToReturn = "Sold Out!";
            wasSoldOut(objectId);
        }
    }else{
        coupToReturn = "Bad Request: Object does not exist.";
        wasSoldOut(objectId);
    }
    return coupToReturn;
}

I don't have a ton of understanding of node.js servers and how they function.

As always, thanks for the help!

like image 902
cgauss Avatar asked Apr 17 '15 19:04

cgauss


People also ask

How do you distribute unique coupon codes?

An email sent to your list. Send an email to your existing list. Yes, this is the most commonly used method, but it ranks as one of the most effective. Emailing your list unique coupon codes makes those who subscribe feel special, like they are privy to exclusive offers not available to everyone else.

Can you stack multiple promo codes?

Coupon stacking means using multiple coupons in a single transaction. For example, when you shop in store at Victoria's Secret, you can use up to three coupons. There's no restriction on whether they're percent-off or dollar-off coupons (combine 5%, 10%, and 25% off coupons in one transaction if you want).


2 Answers

The problem lies in the non-blocking/asynchronous nature of Node. Calls to the same function from simultaneous requests don't wait for each other to finish. A lot of requests come in and concurrently access the global codes array.

You give out the same code multiple times because the counter is incremented by multiple requests concurrently so it can happen that more than one requests sees the same counter state.

One approach to manage the concurrency problem is to allow only one access (to getUserACoupon in your case) at a time, so that part of execution where a coupon is consumed is synchronised or mutually exclusive. One way to achieve this is a locking mechanism so when one request gains access to the lock, further requests wait until the lock is released. In pseudo code it could look something like this:

wait until lock exists
create lock
if any left, consume one coupon
remove lock

But this approach goes against the non-blocking nature of Node and also introduces the problem of who gets the lock when released if more than one request was waiting.

A better way is more likely a queue system. It should work so a code is not consumed at the time of the request but placed in a queue as a callable, waiting to kick off. You can read the length of the queue and stop accepting new requests ("sold out"), however, this will be still concurrent over a global queue/counter so you might end up with a few more queued items than coupons, but this is not a problem because the queue will be processed synchronously so it can be exactly determined when the number of allocated coupons are reached and just give "sold out" to the rest if any and, more importantly, ensure each code is served only once.

Using temporal, it could be quite easy to create a linear, delayed task list:

var temporal = require("temporal");
global.queues = {};

app.post('/getCoupon', urlencodedParser, function(req, res){
   if (!req.body) return res.status(400).send("Bad Request");
   if (!req.body.category) return res.status(200).send("Please Refresh the Page.");

    // Create global queue at first request or return to it.
    var queue;
    if( !global.queues[req.body.objectId] ) {
        queue = global.queues[req.body.objectId] = temporal.queue([]);
    }
    else {
        queue = global.queues[req.body.objectId];
    }

    // Prevent queuing after limit
    // This will be still concurrent access so in case of large 
    // number of requests a few more may end up queued
    if( global.given[objectId] >= global.coupons[objectId].length ) {
        res.type('text/plain');
        res.status(200).send("Sold out!");
        return;
    }

    queue.add([{
      delay: 200,
      task: function() {
        //Go grab a coupon
        var coupon = getUserACoupon(req.body.objectId);

        res.type('text/plain');
        res.status(200).send(coupon);

        if(coupon != "Sold Out!" && coupon != "Bad Request: Object does not exist."){

          //Update user & product analytics
          setStatsAfterCouponsSent(req.body.objectId, req.body.sellerProduct, req.body.userEmail, req.body.purchaseProfileId, coupon, req.body.category);

        }
      }  
    }]);
});

One key point here is that temporal executes the tasks sequentially adding up the delays, so if the delay is more then it is needed for the task to run, no more than one task will access the counter/codes array at once.

You could implement you own solution based on this logic using timed queue processing as well, but temporal seems worth a try.

like image 97
marekful Avatar answered Oct 07 '22 02:10

marekful


Have you considered queueing the http requests as they come in, in order to maintain their order like in this question. How to queue http get requests in Nodejs in order to control their rate?

like image 25
Jake C. Avatar answered Oct 07 '22 02:10

Jake C.