A checkout is a distributed system, whether you designed one or not
Γράφει ο Panagiotis Athanasakopoulos · Παναγιώτης Αθανασακόπουλος
Architecture14 Σεπτεμβρίου 20263 min
Η πιο ακριβή υπόθεση στο web development είναι ότι το checkout είναι μια φόρμα. Μοιάζει με φόρμα: λίγα πεδία, ένα κουμπί, μια σελίδα ευχαριστίας. Τη στιγμή όμως που ο πελάτης πατά «πληρωμή», τουλάχιστον τρία συστήματα πρέπει να συμφωνήσουν για το τι μόλις έγινε. Ο πάροχος πληρωμών που κίνησε τα χρήματα. Το σύστημα που κρατά το απόθεμα ή το δωμάτιο. Και η δική σας βάση δεδομένων, που υποτίθεται ότι θυμάται και τα δύο. Αυτά τα τρία δεν τα δένει μια κοινή συναλλαγή. Τα δένει μόνο η ελπίδα ότι θα συμφωνήσουν.
Στο TRVL, το online ταξιδιωτικό γραφείο που διευθύνω, κάθε κράτηση περνά ακριβώς από αυτά τα τρία: έναν πάροχο πληρωμών, το σύστημα κρατήσεων του ίδιου του ξενοδοχείου και τη βάση μας. Κάθε απόφαση σε εκείνο το checkout ξεκινά από μία υπόθεση: οποιοδήποτε από τα τρία μπορεί να αργήσει, να πει «ναι» δύο φορές ή να μην πει τίποτα. Αν σχεδιάσετε μόνο για την περίπτωση που όλα πάνε καλά, αυτή θα είναι η μόνη περίπτωση που έχετε καλύψει.
Οι διαφωνίες έχουν τρεις μορφές, και κάθε checkout τις συναντά όλες κάποια στιγμή. Η πληρωμή πετυχαίνει, αλλά ο server σας δεν το μαθαίνει ποτέ, γιατί η ειδοποίηση καθυστέρησε, στάλθηκε ξανά ή χάθηκε. Η ειδοποίηση φτάνει δύο φορές, και το σύστημα χειρίζεται το δεύτερο αντίγραφο σαν να ήταν καινούργιο. Ή ο πελάτης κάνει κάτι απολύτως λογικό: αλλάζει τρόπο πληρωμής, βάζει έναν κωδικό έκπτωσης, γυρίζει ένα βήμα πίσω. Κάθε μία από αυτές τις κινήσεις δημιουργεί σιωπηλά μια νέα απόπειρα πληρωμής δίπλα στην παλιά.
Η πρώτη άμυνα είναι η idempotency: κάθε ενέργεια που κινεί χρήματα ή απόθεμα πρέπει να μπορεί να τρέξει δύο φορές χωρίς πρόβλημα. Αυτό σημαίνει ένα κλειδί που ταυτοποιεί την ενέργεια και όχι το αίτημα. Και σημαίνει ότι το επιβάλλει η βάση δεδομένων. Ένα μοναδικό ευρετήριο (unique index) στην αναφορά πληρωμής αξίζει περισσότερο από οποιονδήποτε προσεκτικό κώδικα εφαρμογής. Είναι η μόνη κλειδαριά που αντέχει μια επανεκκίνηση, έναν δεύτερο server και έναν developer που δεν διάβασε το σχόλιο.
Η δεύτερη είναι να αποφασίσετε, μία φορά, τι σημαίνει «πληρώθηκε». Δεν σημαίνει ότι ο browser επέστρεψε στη σελίδα ευχαριστίας. Οι καρτέλες κλείνουν, τα κινητά χάνουν σήμα στο τρένο, και η ανακατεύθυνση στη σελίδα ευχαριστίας είναι απλώς μια ευγένεια προς τον πελάτη, όχι καταγραφή της πληρωμής. Η αλήθεια είναι το γεγονός που στέλνει ο ίδιος ο πάροχος πληρωμών στον server σας, επαληθευμένο, και επιβεβαιωμένο μόνο αφού το έχετε καταγράψει. Η σελίδα που βλέπει ο πελάτης πρέπει να διαβάζει αυτή την αλήθεια, όχι να τη δημιουργεί.
Η τρίτη είναι αυτή που παγιδεύει και έμπειρες ομάδες: οι απόπειρες δεν είναι παραγγελίες. Όταν ένα checkout παράγει πολλές απόπειρες πληρωμής, η παραγγελία πρέπει να ακολουθεί την απόπειρα που πέτυχε, όχι την πρώτη που δημιουργήθηκε. Αν συνδέσετε την κράτηση με την πρώτη απόπειρα και ο πελάτης πληρώσει στην τρίτη, έχετε φτιάξει την κλασική αποτυχία: έναν πελάτη με απόδειξη και μια κράτηση που το σύστημά σας πιστεύει ότι δεν έγινε ποτέ.
Γι' αυτό ένα checkout χρειάζεται μηχανισμό συμφωνίας (reconciler) όσο χρειάζεται και φόρμα. Κάτι πρέπει να συγκρίνει σε τακτά διαστήματα τι λέει ο πάροχος πληρωμών ότι πληρώθηκε με τι λέει η βάση σας ότι εκτελέστηκε. Ό,τι πληρώθηκε αλλά δεν εκτελέστηκε πρέπει να φτάνει σε έναν άνθρωπο, με ειδοποίηση που δεν περνά απαρατήρητη. Εξίσου σημαντικό: ό,τι καθαρίζει εγκαταλελειμμένες παραγγελίες πρέπει να ρωτά τον πάροχο πριν σημειώσει μία ως αποτυχημένη. Μια διαδικασία καθαρισμού που εμπιστεύεται μόνο τη δική σας βάση θα ακυρώσει, αργά ή γρήγορα, μια παραγγελία που κάποιος πλήρωσε.
Μετά υπάρχει η μερική επιτυχία, η περίπτωση που τα περισσότερα checkout κάνουν πως δεν υπάρχει. Ένας πελάτης κλείνει τρία δωμάτια και το ξενοδοχείο επιβεβαιώνει δύο. Η λάθος αντίδραση είναι να ξαναστείλετε όλο το αίτημα. Τα δύο που πέτυχαν είναι πραγματικά, και μια δεύτερη απόπειρα τα κλείνει δύο φορές. Η σωστή είναι να καταγράψετε ακριβώς τι πήρατε, να σταματήσετε, και να δώσετε το υπόλοιπο σε έναν άνθρωπο, για να το ολοκληρώσει με το χέρι ή να επιστρέψει τα χρήματα. Ένα σύστημα που δεν μπορεί να καταγράψει το «δύο στα τρία» θα το καταγράψει ως τρία ή ως κανένα. Και τα δύο είναι ψέματα.
Τίποτα από αυτά δεν είναι εξωτικό. Είναι η συνηθισμένη πειθαρχία των κατανεμημένων συστημάτων: idempotency, μία πηγή αλήθειας, συμφωνία δεδομένων, αντιστάθμιση. Εφαρμόζεται στο ένα σημείο όπου κάθε επιχείρηση έχει πράγματι ένα κατανεμημένο σύστημα, είτε το σχεδίασε κάποιος σε έναν λευκό πίνακα είτε όχι. Η φόρμα είναι το εύκολο κομμάτι. Το προϊόν είναι ό,τι συμβαίνει στα δέκα δευτερόλεπτα μετά το πάτημα του κουμπιού.
Common questions
- Γιατί ένα checkout δεν μπορεί να είναι μία συναλλαγή στη βάση δεδομένων;
- Γιατί τα χρήματα, το απόθεμα και η παραγγελία ζουν σε διαφορετικά συστήματα. Ο πάροχος πληρωμών, το σύστημα κρατήσεων ή αποθήκης και η βάση του καταστήματος καταγράφουν το καθένα ανεξάρτητα, οπότε δεν υπάρχει συναλλαγή που να καλύπτει και τα τρία. Το checkout πρέπει να σχεδιαστεί ώστε να φέρνει τα τρία σε συμφωνία εκ των υστέρων: με ενέργειες που μπορούν να επαναληφθούν χωρίς παρενέργειες (idempotent), με μία συμφωνημένη πηγή αλήθειας για την πληρωμή, και με τακτικό έλεγχο συμφωνίας.
- Τι είναι το idempotency key στις πληρωμές;
- Μια τιμή που ταυτοποιεί μία λογική ενέργεια, μία πληρωμή ή μία κράτηση, ώστε η επανάληψή της να μην έχει κανένα επιπλέον αποτέλεσμα. Αν ένα αίτημα ή μια ειδοποίηση φτάσει δύο φορές, το δεύτερο αναγνωρίζεται από το κλειδί του και αγνοείται. Είναι πιο ισχυρό όταν το επιβάλλει η ίδια η βάση δεδομένων με περιορισμό μοναδικότητας, αντί να βασίζεται στον κώδικα της εφαρμογής για να το ελέγξει πρώτα.
- Η παραγγελία πρέπει να επιβεβαιώνεται από την ανακατεύθυνση ή από το webhook της πληρωμής;
- Από το γεγονός που στέλνει ο server του παρόχου πληρωμών στον δικό σας server, επαληθευμένο και καταγεγραμμένο. Η ανακατεύθυνση του browser μπορεί να μη φτάσει για απλούς λόγους, όπως μια κλειστή καρτέλα ή μια χαμένη σύνδεση. Αν τη θεωρήσετε απόδειξη πληρωμής, θα έχετε πελάτες που πλήρωσαν χωρίς παραγγελία. Κι αν την εμπιστευτείτε χωρίς επαλήθευση, ο καθένας μπορεί να δηλώσει μια πληρωμή που δεν έκανε.