Open source repositories at: github and bitbucket. Current project: erp4all

Wednesday, August 29, 2012

Ένα Twitter clone γραμμένο σε django

Ένα Twitter clone γραμμένο σε django

To dwitter είναι ένα εκπαιδευτικό project σε django. Ο σκοπός του είναι να παρουσιάσει διάφορες τεχνικές και προγράμματα που είναι διαθέσιμα για djangο. Με τον συνδυασμό αυτών και λίγο προγραμματισμό είμαστε σε θέση να φτιάξουμε μια εφαρμογή με τις δυνατότητες του twitter.

Ξεκίνησε σαν ένα project για εκμάθηση των δυνατοτήτων της socket.io. Αυτή είναι μια βιβλιοθήκη γραμμένη σε javascript που υλοποιεί πρωτόκολλα websockets στους διάφορους browser. Με τον όρο websockets αναφερόμαστε γενικά στην δυνατότητα που έχει ένας server να επικοινωνήσει απ' ευθείας με τους clients του μέσω http. Έτσι ενώ εσείς διαβάζετε τα tweets που έχετε στην σελίδα σας, ένα μήνυμα στο πάνω μέρος της σελίδας σας ενημερώνει ότι: "Υπάρχουν 2 νέα tweets". Πως ήρθε αυτό το μήνυμα σε εσάς, αφού δεν έχετε κάνει refresh ή κάποια άλλη ενέργεια; Πως σας ξεχώρισε ανάμεσα από τόσους που είναι συνδεδεμένοι την ίδια στιγμή; Για όλα αυτά είναι υπεύθυνα τα websockets. Για την υλοποίηση των websockets σε python και django χρησιμοποιήθηκε το gevent-socketio. Ένα module βασισμένο στο gevent, το οποίο παρέχει και έναν ειδικό worker για το gunicorn. Το setup αυτό λειτουργεί πολύ καλά με websockets, εξυπηρετεί τις σελίδες της django και ακόμα μοιράζει τα static αρχεία μας.

Ο χρήστης μετά το login μεταφέρεται στην σελίδα με τα dwits (ταιριαστή παράφραση του tweets!) του. Η σελίδα ζητάει από τον server μια σύνδεση socket και τον πληροφορεί ότι θέλει να συμμετάσχει στο δωμάτιο με το όνομα του χρήστη. Όπως στο irc υπάρχουν πολλά rooms (δωμάτια), έτσι κι εδώ κάθε χρήστης έχει το δικό του. Σε αυτό το δωμάτιο ακούει για νέα dwits. Όταν ένας χρήστης δημοσιεύει ένα dwit, αυτό αντιγράφεται σε όλα τα δωμάτια των χρηστών που τον ακολουθούν. Ο server τώρα, όταν έχει κάτι να στείλει από ένα δωμάτιο το στέλνει μέσω του socket που έχει εγγραφεί σε αυτό το δωμάτιο. Έτσι ο χρήστης λαμβάνει μόνο τα dwits που προορίζονται γι' αυτόν. Το ίδιο συμβαίνει και όταν ο χρήστης παρακολουθεί κάποιο tag. Φεύγει από το δικό του δωμάτιο και συνδέεται σε δωμάτιο με το όνομα του tag. Κάθε dwit που δημοσιεύεται και περιέχει tag, αντιγράφεται και σε δωμάτιο με το όνομα του tag.


Από τα παραπάνω είναι φανερό ότι χρειάζεται μια ειδική αντιμετώπιση όσον αφορά στην διαχείριση των dwits. Αυτά αποθηκεύονται στην βάση της django, αλλά για τον διαμοιρασμό τους χρησιμοποιούμε την πολύ γρήγορη redis και συγκεκριμένα την δυνατότητά της pub/sub. Με αυτήν δίνεται η δυνατότητα σε μια function να εγγραφεί (subscribe) σε ένα "κανάλι". Όταν κάποιος δημοσιεύσει (publish) σε αυτό το κανάλι τότε η function εκτελείται με τα δεδομένα της δημοσίευσης. Έτσι το dwit σώζεται στην βάση και αντιγράφεται στα ανάλογα κανάλια (ακόλουθοι και tags). Όποιο κανάλι έχει συνδρομητές (sockets) τους αποστέλλεται το περιεχόμενο και αυτοί με την σειρά τους το στέλνουν στην σελίδα του χρήστη.


Κάνοντας λίγες πράξεις βλέπουμε ότι αυτό το πλάνο θα έχει πρόβλημα αν εκτελείται σειριακά. Αν ο κάθε χρήστης έχει Χ ακόλουθους και το κάθε dwit μπορεί να έχει Υ tag και να δημοσιεύονται ταυτόχρονα Ζ dwits, o πολλαπλασιασμός αυτός γρήγορα θα φτάσει σε σημείο όπου η django δεν θα προλαβαίνει να δείξει σελίδες. Γι' αυτό, το κομμάτι αυτό, το βάζουμε σε μια ουρά. Κάθε τι που είναι να δημοσιευθεί, θα δημοσιευθεί με την σειρά που μπήκε στην ουρά και θα περιμένει όση ώρα χρειαστεί, αφήνοντας την εφαρμογή μας να μοιράζει σελίδες. Αυτήν την δουλειά κάνει το celery. Κάθε function που δηλώνεται ως task του celery, μεταφέρεται στην ουρά (στην προκειμένη περίπτωση πάλι στην redis) και το celery θα την εκτελέσει όταν μπορέσει. Υπάρχει μεγάλος φόρτος εργασίας; Ξεκινάμε περισσότερους εργάτες (workers) του celery.


Η βάση της django έχει τρία βασικά μοντέλα. Τους χρήστες της εφαρμογής, τα dwits και τα dwits per follower (dwits ανά ακόλουθο). Στο τελευταίο μοντέλο για κάθε dwit που δημοσιεύει ένας χρήστης δημιουργούνται εγγραφές με το id του dwit και το id του κάθε ακόλουθου του χρήση. Έτσι αν κάποιος έχει 1000 ακόλουθους και έχει κάνει 1000 dwits θα έχει 1 εκ. εγγραφές σε αυτό το μοντέλο. Μόνο αυτός. Αν έχεις μόνο (για τα δεδομένα του twitter) 1000 τέτοιους χρήστες έφτασες εύκολα 1 δισ. εγγραφές. Φαίνεται αμέσως ότι μια κλασική βάση δεδομένων θα έχει πρόβλημα. Γι' αυτό θα ήταν καλό να χρησιμοποιηθεί μια NoSQL βάση σαν την redis (σχετικό άρθρο εδώ). Από την άλλη όμως αυτό είναι ένα απλό project και δεν συναγωνίζεται το twitter. Να αναφέρουμε εδώ και τις εκπληκτικές δυνατότητες του django ORM, όπου για παράδειγμα στο μοντέλο των χρηστών υπάρχει ένα πεδίο ManyToMany με τον εξής ορισμό:


following = models.ManyToManyField('self', blank=True,\
                   symmetrical=False, related_name='followers')
Αυτό το πεδίο δείχνει ποιους χρήστες ακολουθεί ο κάθε χρήστης. Κάνοντας:

member = Member.objects.get(pk=10)
following = member.following.all()
έχουμε τους χρήστες που ακολουθεί ο χρήστης με id=10. Αλλά κάνοντας και:

followers = member.followers.all()
έχουμε όλους που τον ακολουθούν. Χωρίς να γράψουμε άλλο κώδικα, εύκολα και ευανάγνωστα. Κάτι τέτοιο δεν υπάρχει σε NoSQL.

Για την εγγραφή των χρηστών χρησιμοποιήσαμε μια δικιά μας έκδοση του django-registration και για την αυθεντικοποίηση των χρηστών μια ελαφρώς αλλαγμένη έκδοση του django.contrib.auth. Το δεύτερο έχει αντιγραφεί όπως είναι από το source (εκτός από κάτι αλλαγές στις μεταφράσεις και στην φόρμα login) και προσθέσαμε την δυνατότητα όταν στέλνει email, να το στέλνει σαν task στο celery. Με αυτό τον τρόπο δεν περιμένει να τελειώσει η αποστολή και μετά να επιστραφεί ο έλεγχος στην django. Το ίδιο κάναμε και στο registration. Αυτός είναι και ο πιο διαδεδομένος τρόπος χρήσης του celery. Για δουλειές που δεν υπεύθυνος ο server της εφαρμογής. Όπως παρακάτω με το haystack.


Το haystack είναι ένα module αναζήτησης. Προσφέρει όλες τις απαραίτητες συναρτήσεις για την αναζήτηση στοιχείων σε μια βάση, χρησιμοποιώντας διάφορες μηχανές αναζήτησης διαθέσιμες στην python. Η αναζήτηση δεν γίνεται απ' ευθείας στην βάση αλλά σε ένα δικό του αρχείο στο οποίο έχει αποθηκεύσει τα στοιχεία που του έχουμε ορίσει ότι είναι αναζητήσιμα. Αυτό το αρχείο πρέπει να ενημερώνεται σε τακτά χρονικά διαστήματα με τα καινούργια στοιχεία της βάσης. Γι αυτόν τον σκοπό χρησιμοποιούμε μια άλλη δυνατότητα του celery, την δυνατότητα προγραμματισμού (crontab) εργασιών. Έχουμε ορίσει μια συγκεκριμένη συνάρτηση, που πραγματοποιεί την ενημέρωση, να εκτελείται κάθε 15 λεπτά.


Για την διαχείριση των tag χρησιμοποιήσαμε το django-taggit και για την εμφάνιση της εφαρμογής το bootstrap. Αυτό είναι ένα πολύ ωραίο css framework, γραμμένο από τα παιδιά του twitter και η προσθήκη δικών μας css είναι ελάχιστη. Χρησιμοποιώντας τα media queries, το bootstrap προσαρμόζεται αυτόματα σε κινητές και pad συσκευές.


Το dwitter είναι διαθέσιμο στο github και περιμένει περισσότερο πειραματισμό και hacking. Οδηγίες εγκατάστασης και παραμετροποίησης βρίσκονται στο README και στο wiki.Υπάρχει και ένα live demo site στην διάθεση σας για να δείτε την λειτουργία του. Μπορείτε να δημιουργήσετε δύο χρήστες με το ίδιο email (κανονικό ή ψεύτικό, δεν συγκεντρώνουμε email) και να κάνετε login από δύο διαφορετικούς browser (firefox και chrome ή desktop και κινητό) και να δείτε την μετάδοση των dwit σε πραγματικό χρόνο από τον ένα στον άλλο.


Καλό hacking.

Andreas Porevopoulos
Lead developer at
http://www.dream-solutions.gr