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!
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.
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).
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.
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?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With