Building Real-Time Voting Mechanisms Using Short and Long Polling
Real-Time Data Monitoring Requirements
Users interacting with the voting interface require immediate visibility into voting statistics without manual page refreshes. To address this, we implement two strategies: standard polling and long polling. The latter is preferred as it significantly reduces the number of network requests while maintaining high data freshness.
1. Implementation via Short Polling
The fundamental concept involves setting up a client-side interval timer. This timer triggers an AJAX request to the server every few seconds (e.g., 3 seconds) to retrieve the current state of the votes.
from flask import Flask, render_template, request, ify
app = Flask(__name__)
# Sample data store
candidates = {
'101': {'name': 'Alice', 'votes': 1},
'102': {'name': 'Bob', 'votes': 0},
'103': {'name': 'Charlie', 'votes': 0},
}
@app.route('/')
def index():
return render_template('vote.html', data=candidates)
@app.route('/cast_vote', methods=['POST'])
def cast_vote():
cid = request.form.get('id')
if cid in candidates:
candidates[cid]['votes'] += 1
return ify({'status': 'success'})
@app.route('/get_results', methods=['GET'])
def get_results():
return ify(candidates)
if __name__ == '__main__':
app.run(threaded=True)
<!DOCTYPE html>
<html>
<head>
<title>Live Voting</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
li { cursor: pointer; padding: 5px; border-bottom: 1px solid #eee; }
</style>
</head>
<body>
<ul id="list">
{% for key, val in data.items() %}
<li uid="{{key}}">{{ val.name }}: {{ val.votes }}</li>
{% endfor %}
</ul>
<script>
function updateList(items) {
$('#list').empty();
$.each(items, function(k, v) {
const li = $('<li>').attr('uid', k).text(`${v.name}: ${v.votes}`);
$('#list').append(li);
});
}
function pollServer() {
$.get('/get_results', function(resp) {
updateList(resp);
});
}
// Poll every 3 seconds
setInterval(pollServer, 3000);
// Handle voting
$('#list').on('click', 'li', function() {
const uid = $(this).attr('uid');
$.post('/cast_vote', {id: uid}, function() {
pollServer(); // Instant update on action
});
});
</script>
</body>
</html>
Limitation: This approach may result in high server load due to frequent requests, and data freshness is limited by the polling interval.
2. Implementation via Long Polling
Long polling optimizes resource usage. The server holds the client's request open until new data is available or a timeout occurs.
Core Logic:
- Client: Sends a request. Upon receiving a response, it updates the DOM and immediately initiates the next request.
- Server: If no new votes exist, the server blocks the request (using a queue mechanism) for a specific duration (e.g., 10 seconds). If a vote occurs during this wait, the server immediately pushes the updated data to all waiting clients. If the timeout is reached, it returns an empty response.
The server-side implementation relies on the queue module to block execution:
import queue
import time
# Create a blocking queue
msg_queue = queue.Queue()
try:
# This blocks execution for 10 seconds unless data is available
data = msg_queue.get(timeout=10)
print(f"Received: {data}")
except queue.Empty:
print("Timeout reached, no data available")
Backend Implementation (app.py):
from flask import Flask, render_template, request, ify, session
import uuid
import queue
app = Flask(__name__)
app.secret_key = 'super_secret_key'
candidates = {
'101': {'name': 'Alice', 'votes': 1},
'102': {'name': 'Bob', 'votes': 0},
'103': {'name': 'Charlie', 'votes': 0},
}
# Dictionary to store user-specific queues
client_queues = {}
@app.route('/')
def index():
# Generate unique ID and queue for the session
sess_id = str(uuid.uuid4())
client_queues[sess_id] = queue.Queue()
session['user_id'] = sess_id
return render_template('long_poll_vote.html', data=candidates)
@app.route('/cast_vote', methods=['POST'])
def cast_vote():
cid = request.form.get('id')
if cid in candidates:
candidates[cid]['votes'] += 1
# Push update to all connected clients
for q in client_queues.values():
try:
q.put(candidates)
except:
pass
return ify({'status': 'success'})
@app.route('/listen', methods=['GET'])
def listen():
sess_id = session.get('user_id')
q = client_queues.get(sess_id)
response = {'update': False, 'data': None}
if q:
try:
# Wait for data or timeout
data = q.get(timeout=10)
response['update'] = True
response['data'] = data
except queue.Empty:
# Timeout: return empty response to trigger reconnect
pass
return ify(response)
if __name__ == '__main__':
app.run(threaded=True)
Frontend Implementation (long_poll_vote.html):
<!DOCTYPE html>
<html>
<head>
<title>Live Voting - Long Polling</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
li { cursor: pointer; padding: 5px; border-bottom: 1px solid #eee; }
</style>
</head>
<body>
<ul id="list">
{% for key, val in data.items() %}
<li uid="{{key}}">{{ val.name }}: {{ val.votes }}</li>
{% endfor %}
</ul>
<script>
function updateList(items) {
$('#list').empty();
$.each(items, function(k, v) {
const li = $('<li>').attr('uid', k).text(`${v.name}: ${v.votes}`);
$('#list').append(li);
});
}
function longPoll() {
$.get('/listen', function(resp) {
if (resp.update) {
updateList(resp.data);
}
// Recursive call immediately after response
longPoll();
});
}
// Start the long polling loop
$(document).ready(function() {
longPoll();
});
// Handle voting
$('#list').on('click', 'li', function() {
const uid = $(this).attr('uid');
$.post('/cast_vote', {id: uid});
});
</script>
</body>
</html>