I have built a simple wish list app in Tornado. The user adds URL to the product and the app keeps tracking its price. The flow is simple, the user logs in, there is a form and user pastes/enters the url in the form and clicks submit. This makes a post request to my server and that url will be added to database.
For submit I am using ajax. So, it submits the request and refreshes the wishlist table in the page. However as this takes time, the users think the app is not working and tend to press 'submit' multiple times. It takes time, as my server verifies the URL, fetches its price, image and other details.
During initial stages, I was talking directly to Tornado server. When I press submit button multiple times, Tornado would add it only once. I don't know how it managed to ignore same requests or how it figured out its same request, which is being processed. Since it never occurred, I never thought about it.
Now there are four instances of Tornado are running behind a Nginx server. So I guess Nginx is passing the request to different tornado instances when multiple times submit is pressed.
So how do I avoid this?
I could create a local storage in browser for every session and maintain list of URLs. When submit is pressed and if URL is already present in the list, then don't send the request. And I would destroy this storage whenever tab is closed.
disable submit button once the url is submitted and till it receives reply from the server
give a facebook style notification when the submit is pressed and hope user does not press submit again.
configure nginx load balancer to work on ip-hash mode. so that the user always gets to served by same instance of Tornado
may be configure nginx somehow that it ignores same POST requests as single instance of Tornado was doing earlier?
Here's the code in question (not sure if it actually matters): http://pastebin.com/u3ZsdmbZ
--
You received this message because you are subscribed to the Google Groups "Tornado Web Server" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python-tornad...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
I followed @A. Jesse Jiryu Davis advice and here are the changes I made.
First I created a unique single index (I don't need compounded one, as _id is already indexed and I just wanted only url field in product document to be unique)
product_db.create_index('url', unique=True, dropDups=True)Do note that above one will remove all the duplicate documents which have same url. If you don't want that, then do following:
product_db.create_index('url', unique=True)If there are any duplicate keys exists, then PyMongo will throw DuplicateKeyError exception.
I changed my javascript code, now at start of ajax it disables the submit button and re-enables it later:
<script type="text/javascript">
$(document).ready(function()
{
$("#product-add-form").on('submit',function()
{
var product_url = $("#product-url").val();
console.log(product_url)
var dataString = 'product-url='+ product_url
$.ajax({
type: "POST",
url: "products",
data: dataString,
success: function() {
$('#product-table-div').load("/products #product-table-div")
}
});
return false;
});
})
.ajaxStart(function(){
$("#prodadd-btn").attr("disabled", "disabled");
NProgress.start();
})
.ajaxStop(function(){
$("#prodadd-btn").removeAttr("disabled");
NProgress.done();
});I also used Nprogress to show a cool progress bar.
These changes should be sufficient to prevent user entering same url multiple times. However there is a possibility that two users could enter same url at the same time. Now as I have created a unique single index, the second insertion will throw DuplicateKeyError. In that case I will just find that url by from db and add it to user. Here is the modified code:
def post(self):
user_email = self.get_secure_cookie('trakr')
user_db = self.application.db.users
product_db = self.application.db.products
tracker_db = self.application.db.trackers # this has product_id to users_id
product_url = self.get_argument('product-url', None)
# ...
product_doc = product_db.find_one({'url': url})
if not product_doc:
# ...
# ...
product_url, product_name, product_img_url, product_price = vendor_func_map[vendor_id](url)
try:
product_id = str(product_db.insert({
# ...
'url': url,
'current_price': product_price,
}))
except pymongo.errors.DuplicateKeyError:
product_doc = product_db.find_one({'url': url})
product_id = product_doc['_id']
else:
product_id = product_doc['_id']
user_db.update({'email_id': user_email}, {'$addToSet': {'tracked_products': ObjectId(product_id)}})
# ...
self.redirect('/products')--
--