Τti είναι η ταυτοδυναμία (idempotency); Γιατί τα API λογιστικής και πληρωμών την χρειάζονται
Πώς μια επαναλαμβανόμενη αίτηση δημιουργεί διπλό τιμολόγιο, τι εγγυάται ένα κλειδί ταυτοδυναμίας, ποιους τρόπους αστοχίας πρέπει να διαχειριστεί και πώς να την υλοποιήσετε σωστά.
Μια λειτουργία είναι ταυτοδύναμη (idempotent) εάν η εκτέλεσσή της δύο φορές έχει το ίδιο αποτέλεσμα με την εκτέλεσσή της μία φορά. Η ανάγνωση μιας εγγραφής είναι από τη φύση της ταυτοδύναμη. Η δημιουργία ενός τιμολογίου δεν είναι: καλέστε το δύο φορές και θα έχετε δύο τιμολόγια, δύο απαιτήσεις και έναν πελάτη που ρωτά γιατί χρεώθηκε διπλά.
Αυτό το κενό έχει σημασία επειδή τα δίκτυα αποτυγχάνουν με έναν συγκεκριμένο, δυσάρεστο τρόπο: η αίτηση πετυχαίνει και η απάντηση χάνεται. Ο πελάτης βλέπει λήξη χρονικού ορίου (timeout) και δεν μπορεί να διακρίνει αν η αίτηση "δεν έφτασε ποτέ" ή "έφτασε, λειτούργησε, αλλά η απάντηση χάθηκε". Η μόνη ασφαλής επιλογή που διαθέτει είναι να επιχειρήσει εκ νέου την αποστολή, και αυτή ακριβώς η επανάληψη είναι που δημιουργεί το διπλό αντίγραφο.
Γιατί η λογιστική καθιστά την κατάσταση χειρότερη
Στα περισσότερα συστήματα, ένα διπλό αντίγραφο είναι μια απλή ενόχληση. Σε ένα καθολικό (ledger), αποτελεί σύνθετο πρόβλημα:
- Ένα διπλό τιμολόγιο καταναλώνει έναν αριθμό από μια ακολουθία χωρίς κενά (gapless sequence), επομένως η διαγραφή του δεν αποτελεί επιλογή: η αμεταβλητότητα σημαίνει ότι η διόρθωσή του απαιτεί πιστωτικό τιμολόγιο, το οποίο είναι ένα έγγραφο που βλέπει ο πελάτης σας.
- Το διπλό αντίγραφο καταχωρεί εγγραφές στο ημερολόγιο, οπότε τα έσοδα, ο ΦΠΑ και οι απαιτήσεις εμφανίζονται όλα αυξημένα μέχρι να το προσέξει κάποιος.
- Εισέρχεται στη δήλωση ΦΠΑ και, στη Λιθουανία, στα μητρώα i.SAF.
- Εάν ενεργοποίησε μια πληρωμή, τα χρήματα μεταφέρθηκαν πραγματικά.
Η λύση "απλός έλεγχος για διπλότυπα" δεν λειτουργεί επίσης, επειδή δύο γνήσια ξεχωριστά τιμολόγια προς τον ίδιο πελάτη για το ίδιο ποσό την ίδια μέρα είναι απολύτως φυσιολογικά. Το σύστημα δεν μπορεί να συμπεράνει την πρόθεση από το περιεχόμενο: ο πελάτης πρέπει να την δηλώσει ρητά.
Πώς λειτουργούν τα κλειδιά ταυτοδυναμίας (idempotency keys)
Ο πελάτης δημιουργεί ένα μοναδικό κλειδί ανά λογική λειτουργία και το αποστέλλει μαζί με την αίτηση:
POST /v1/sales/invoices/create
Idempotency-Key: 8f14e45f-ea1b-4c2b-9f3a-2d7c1e0b5a91
Στη συνέχεια, ο διακομιστής εγγυάται τα εξής:
- Πρώτη αίτηση με αυτό το κλειδί: εκτέλεση κανονικά, αποθήκευση του αποτελέσματος σε αντιστοίχιση με το κλειδί.
- Επανάληψη με το ίδιο κλειδί και το ίδιο ωφέλιμο φορτίο (payload): καμία επανεκτέλεση, επιστροφή της αποθηκευμένης απάντησης.
- Ίδιο κλειδί, διαφορετικό ωφέλιμο φορτίο: απόρριψη. Το κλειδί αποτελεί δήλωση ότι πρόκειται για την ίδια λειτουργία. Ένα διαφορετικό σώμα σημαίνει ότι δεν είναι, και η σιωπηλή επιστροφή του παλιού αποτελέσματος θα ήταν χειρότερη από ένα σφάλμα.
- Επανάληψη ενώ η αρχική αίτηση εξακολουθεί να εκτελείται: απόρριψη με σφάλμα που επιδέχεται επανάληψη (retryable error) αντί για ταυτόχρονη εκτέλεση.
Το τέταρτο σημείο είναι αυτό που συχνά παραλείπουν οι απλοϊκές υλοποιήσεις. Ένας πελάτης του οποίου η αίτηση λήγει στα 5 δευτερόλεπτα και επιχειρεί επανάληψη στα 6 θα συγκρουστεί με τη δική του εκκρεμή αίτηση. Χωρίς προστασία έναντι του ταυτόχρονου φόρτου (concurrency guard), προχωρούν και οι δύο και δημιουργείται το διπλό αντίγραφο που ο μηχανισμός προοριζόταν να αποτρέψει.
Σωστή δημιουργία κλειδιών
Το κλειδί πρέπει να προσδιορίζει τη λειτουργία και όχι την προσπάθεια:
- Σωστό: ένα UUID που δημιουργείται μία φορά όταν ο χρήστης κάνει κλικ στην επιλογή "έκδοση τιμολογίου" και επαναχρησιμοποιείται για κάθε επανάληψη αυτού του κλικ.
- Σωστό: ένα ντετερμινιστικό κλειδί που προέρχεται από το δικό σας domain, όπως
order-1041-invoice. - Λάθος: ένα νέο UUID ανά προσπάθεια HTTP. Κάθε επανάληψη λαμβάνει ένα νέο κλειδί, επομένως κάθε επανάληψη είναι μια νέα λειτουργία και δεν έχετε υλοποιήσει τίποτα.
- Λάθος: ένα hash του ωφέλιμου φορτίου. Δύο απολύτως πανομοιότυπες λειτουργίες συγκρούονται τότε μεταξύ τους και η δεύτερη καταπίνεται σιωπηρά.
Τα κλειδιά χρειάζονται μια περίοδο διατήρησης (retention period): αρκετά μεγάλη ώστε να καλύπτει ρεAλιστικά παράθυρα επανάληψης και διακοπές λειτουργίας του πελάτη, αλλά όχι για πάντα.
Τι δεν επιλύει η ταυτοδυναμία
Η ταυτοδυναμία προστατεύει από την διπλή εκτέλεση της ίδιας αίτησης. Δεν προστατεύει από:
- Δύο διαφορετικούς χρήστες που δημιουργούν το ίδιο τιμολόγιο από διαφορετικές συνεδρίες. Αυτό είναι πρόβλημα επιχειρησιακών κανόνων (business-rule problem) και λύνεται με περιορισμούς μοναδικότητας, όπως ένα τιμολόγιο ανά παραγγελία.
- Λογικά διπλότυπα με διαφορετικά κλειδιά.
- Μη ντετερμινισμό εντός της λειτουργίας: εάν ο χειριστής (handler) δημιουργήσει μια χρονική σήμανση (timestamp) ή έναν αριθμό, η αποθηκευμένη απάντηση καταγράφει την πρώτη, η οποία είναι η επιθυμητή συμπεριφορά, αλλά αξίζει να γίνει κατανοητή.
Λειτουργεί συνδυαστικά και όχι αντικαταστατικά με τους περιορισμούς της βάσης δεδομένων. Το καθολικό της Nordlet χρησιμοποιεί και τα δύο: τα κλειδιά ταυτοδυναμίας σταματούν τις επαναλαμβανόμενες αιτήσεις, ενώ τα μοναδικά ευρετήρια (unique indexes) σταματούν τις διπλές παρτίδες διακανονισμού ανά αναγνωριστικό πληρωμής (payout ID), τα διπλά τιμολόγια αγοράς ανά προμηθευτή και αριθμό εγγράφου, καθώς και τις διπλές εκτελέσεις αποσβέσεων ανά πάγιο στοιχείο και περίοδο.
Πώς το υλοποιεί η Nordlet
Αποστείλετε μια κεφαλίδα Idempotency-Key σε οποιαδήποτε αίτηση αλλάζει κατάσταση: μια μη κενή συμβολοσειρά έως 255 χαρακτήρων.
Η πρώτη αίτηση με ένα δεδομένο κλειδί εκτελείται και το αποτέλεσμά της αποθηκεύεται. Μια επανάληψη με το ίδιο κλειδί και το ίδιο ωφέλιμο φορτίο επιστρέφει την αποθηκευμένη απάντηση χωρίς να εκτελεστεί ξανά, οπότε το τιμολόγιο δημιουργείται ακριβώς μία φορά, ανεξάρτητα από το πόσες φορές επαναλαμβάνεται η κλήση.
Δύο τρόποι αστοχίας είναι ρητοί αντί για σιωπηροί:
- Ίδιο κλειδί, διαφορετικό ωφέλιμο φορτίο -> απόρριψη: "Idempotency-Key was already used with a different request payload." Αυτό εντοπίζει την πραγματικά επικίνδυνη περίπτωση όπου ένας πελάτης επαναχρησιμοποιεί ένα κλειδί ενώ αλλάζει το ποσό.
- Ίδιο κλειδί, η αρχική αίτηση εξακολουθεί να εκτελείται -> απόρριψη ως επαναλαμβανόμενη: "A request with this Idempotency-Key is still being processed, retry shortly." Αποτροπή ταυτόχρονης διπλής εκτέλεσης.
Τα κλειδιά οριοθετούνται ανά εταιρεία, οπότε δύο μισθωτές (tenants) που δημιουργούν το ίδιο UUID δεν συγκρούονται ποτέ.
Η συμπληρωματική εγγύηση λειτουργεί προς την αντίθετη κατεύθυνση. Η παράδοση ειδοποιήσεων webhook πραγματοποιείται τουλάχιστον μία φορά (at-least-once), πράγμα που σημαίνει ότι το τελικό σημείο (endpoint) σας θα λαμβάνει περιστασιακά το ίδιο συμβάν δύο φορές: μετά από λήξη χρονικού ορίου όπου η παράδοση πέτυχε πράγματι, ή κατά την επαναπαράδοση. Κάθε συμβάν φέρει ένα αναγνωριστικό και ο χειριστής σας πρέπει να καταγράφει τα επεξεργασμένα αναγνωριστικά και να αγνοεί τις επαναλήψεις. Η ταυτοδυναμία είναι μια ιδιότητα που χρειάζονται και τα δύο άκρα μιας ενοποίησης. Ο οδηγός συμβάσεων API καλύπτει λεπτομερώς την πλευρά των αιτήσεων.
FAQ
Τι είναι ένα κλειδί ταυτοδυναμίας;
Ένα αναγνωριστικό που δημιουργείται από τον πελάτη και αποστέλλεται με μια αίτηση, επιτρέποντας στον διακομιστή να αναγνωρίσει μια επανάληψη της ίδιας λογικής λειτουργίας. Η πρώτη αίτηση εκτελείται, ενώ οι επαναλήψεις με το ίδιο κλειδί επιστρέφουν το αποθηκευμένο αποτέλεσμα αντί να εκτελεστούν ξανά.
Γιατί τα API λογιστικής χρειάζονται την ταυτοδυναμία;
Επειδή μια διπλή εγγραφή δημιουργεί ένα πραγματικό έγγραφο με πραγματικές συνέπειες: ένα δεύτερο τιμολόγιο που καταναλώνει έναν αύξοντα αριθμό, καταχωρείται στο καθολικό και εισέρχεται σε μια δήλωση ΦΠΑ. Σε αντίθεση με μια διπλή ανάγνωση, δεν μπορεί απλώς να διαγραφεί, αλλά πρέπει να διορθωθεί με πιστωτικό τιμολόγιο.
Πρέπει να δημιουργώ ένα νέο κλειδί για κάθε επανάληψη;
Όχι, αυτό ακυρώνει εντελώς τον μηχανισμό. Δημιουργήστε ένα κλειδί ανά λογική λειτουργία και επαναχρησιμοποιήστε το για κάθε επανάληψη αυτής της λειτουργίας. Ένα νέο κλειδί ανά προσπάθεια σημαίνει ότι κάθε προσπάθεια αντιμετωπίζεται ως νέα λειτουργία.
Τι συμβαίνει εάν επαναχρησιμοποιήσω ένα κλειδί με διαφορετικό ωφέλιμο φορτίο;
Η αίτηση απορρίπτεται. Το κλειδί διαβεβαιώνει ότι πρόκειται για την ίδια λειτουργία, επομένως ένα αλλαγμένο σώμα σημαίνει ότι κάτι δεν πάει καλά, πιθανότατα πρόκειται για σφάλμα επαναχρησιμοποίησης κλειδιού από την πλευρά του πελάτη. Η σιωπηλή επιστροφή του προηγούμενου αποτελέσματος θα ήταν χειρότερη.
Αποτρέπει η ταυτοδυναμία όλα τα διπλότυπα;
Όχι. Αποτρέπει τη διπλή εκτέλεση της ίδιας αίτησης. Δύο διαφορετικοί χρήστες που δημιουργούν ανεξάρτητα το ίδιο τιμολόγιο έχουν κάνει δύο διαφορετικές αιτήσεις, το οποίο αποτελεί πρόβλημα επιχειρησιακών κανόνων για τους περιορισμούς μοναδικότητας και όχι κάτι που μπορεί να εντοπίσει ένα κλειδί ταυτοδυναμίας.