
Μια χαμένη σύνδεση δεν θα έπρεπε να ρίχνει όλη την εφαρμογή — αλλά η δική μας το έκανε
Όταν το backend μας έπεσε στη μέση μιας σύσκεψης, δεν σταμάτησε απλώς τη μεταγραφή — έριξε ολόκληρη την εφαρμογή. Η αιτία ήταν ένα μόνο ανεπεξέργαστο συμβάν, και η διόρθωση ήταν δέκα γραμμές. Αυτή είναι η ομάδα εκδόσεων που έκανε το GeekBye να μένει συνδεδεμένο στον λογαριασμό και συνδεδεμένο στο δίκτυο μέσα από τα πράγματα που παλιά το σκότωναν.
Κάποια bugs διακόπτουν μια λειτουργία. Τα χειρότερα ρίχνουν όλα όσα βρίσκονται γύρω της. Σε μια πρώιμη έκδοση του GeekBye, αν το backend μας έπεφτε ενώ ήσασταν στη μέση μιας σύσκεψης, η μεταγραφή δεν σταματούσε απλώς — ολόκληρη η εφαρμογή κρασάριζε. Μία χαμένη σύνδεση, και όλο το παράθυρο χανόταν.
Η ομάδα εκδόσεων που το διόρθωσε αυτό — από v1.6.8 έως v1.6.11 — είναι ένα μάθημα υψηλού επιπέδου στην αδοξη δουλειά του «μείνε συνδεδεμένος στον λογαριασμό, μείνε συνδεδεμένος στο δίκτυο». Η κεντρική διόρθωση ήταν δέκα γραμμές. Ορίστε ποιες ήταν αυτές οι δέκα γραμμές, και όλα τα υπόλοιπα που βγήκαν μαζί τους.
Δέκα γραμμές ανάμεσα σε μια αποσύνδεση και ένα κρασάρισμα
Το κρασάρισμα ζούσε στον κώδικα ήχου. Όταν μια συνεδρία μεταγραφής αποδομούσε τα WebSockets της, καλούσε removeAllListeners() σε αυτά — λογικό καθάρισμα. Αλλά ορίστε η λεπτομέρεια του Node.js που μετατρέπει το καθάρισμα σε καταστροφή: ένα συμβάν error χωρίς ακροατή δεν αγνοείται. Ξαναρίχνεται ως μη συλληφθείσα εξαίρεση.
Φανταστείτε λοιπόν τη σειρά όταν το backend πέφτει: το socket έχει ένα συμβάν error στην ουρά, έτοιμο να πυροδοτηθεί. Το καθάρισμα τρέχει removeAllListeners(), αφαιρώντας τον χειριστή που θα το είχε πιάσει. Μια στιγμή αργότερα το σφάλμα της ουράς πυροδοτείται — στο κενό. Το Node βλέπει ένα συμβάν error που κανείς δεν ακούει, και κάνει το μόνο που του επιτρέπει το συμβόλαιό του: το ρίχνει ως μη συλληφθείσα εξαίρεση, που κρασάρει τη διεργασία. Το backend του χρήστη τραύλισε για δύο δευτερόλεπτα και η εφαρμογή τους εξαφανίστηκε.
Η διόρθωση (v1.6.8) είναι σχεδόν αντικλιμακτική. Αμέσως μετά την αφαίρεση των ακροατών, επανασύνδεσε έναν σκόπιμο no-op χειριστή error:
this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events
Αυτή η κενή συνάρτηση είναι όλο το νόημα. Δίνει στο εκκρεμές σφάλμα ένα μέρος να προσγειωθεί — έναν απορροφητή — ώστε το Node να έχει έναν ακροατή και να μην ξαναρίχνει ποτέ. Το ίδιο commit καθάρισε επίσης τα μισάνοιχτα sockets όταν μια απόπειρα σύνδεσης αποτυγχάνει στα μισά, ώστε ένα αποτυχημένο connect να μην μπορεί να αφήσει ένα ζωντανό socket να κρέμεται με ένα σφάλμα έτοιμο να πυροδοτηθεί. Το σχόλιο του commit το λέει καθαρά: «Χωρίς αυτό, τα εκκρεμή σφάλματα γίνονται μη συλληφθείσες εξαιρέσεις και κρασάρουν την εφαρμογή.» Δέκα γραμμές μετέτρεψαν ένα πλήρες κρασάρισμα σε μια σιωπηλή, ανακτήσιμη αποσύνδεση.
Το μάθημα αξίζει περισσότερο από τη διόρθωση: κάθε φορά που καλείς removeAllListeners() σε ένα socket ή stream που μπορεί να το αποδομείς ακόμα, επανασύνδεσε πρώτα έναν no-op απορροφητή error. Ένα ανεπεξέργαστο συμβάν error είναι κρασάρισμα, όχι μια γραμμή log.
Παραμονή συνδεδεμένος: ανανέωσε εκ των προτέρων, και ανανέωσε στο αναπήδημα
Το μισό «μείνε συνδεδεμένος στο δίκτυο» είχε ένα δίδυμο «μείνε συνδεδεμένος στον λογαριασμό». Πριν από αυτή την ομάδα, το token αυθεντικοποίησης του GeekBye απλώς έληγε — μετά από λίγο σας πετούσε ανερμήνευτα πίσω στην οθόνη σύνδεσης, στη μέση της ροής εργασίας, χωρίς κανέναν λόγο ορατό σε εσάς.
Η v1.6.8 το διόρθωσε από τις δύο κατευθύνσεις:
- Προληπτικά: ένα timer ανανεώνει το token πριν λήξει, προγραμματισμένο στη λήξη μείον ένα έξυπνο buffer (ένα κλάσμα του χρόνου ζωής του token, με κατώτατο όριο και ταβάνι). Ανανεώνεστε πριν το αντιληφθείτε ποτέ.
- Αντιδραστικά: αν ένα αίτημα επιστρέψει ούτως ή άλλως
401— απόκλιση ρολογιού, ένα χαμένο timer, ένα κρύο laptop — ο client ανανεώνει μία φορά και επαναλαμβάνει το αρχικό αίτημα ακριβώς μία φορά. Ένα μόνο flag φύλαξης περιορίζει αυτή την επαναπροσπάθεια, ώστε ένα επίμονο 401 να μην μπορεί ποτέ να μπει σε ατελείωτο βρόχο.
Το λεπτό-αλλά-κρίσιμο σημείο: και τα δύο μονοπάτια είναι πίσω από ένα mutex. Αν δέκα αιτήματα χτυπήσουν ένα 401 την ίδια στιγμή, δεν πυροδοτούν δέκα ανανεώσεις — όλα περιμένουν τη μία ανανέωση σε εξέλιξη και μετά επαναλαμβάνουν. Και όταν μια ανανέωση πραγματικά δεν μπορεί να σωθεί, η εφαρμογή λήγει τη συνεδρία ομαλά — μια καθαρή ειδοποίηση δέκα δευτερολέπτων «η συνεδρία σας έληξε, παρακαλώ συνδεθείτε ξανά» — αντί για μια σιωπηλή κλωτσιά προς την οθόνη σύνδεσης.
Επανασύνδεση: πρόσθεσέ την στον client, μετά μετακίνησέ την στο backend
Η v1.6.9 έκανε μια χαμένη σύνδεση μεταγραφής ανακτήσιμη αντί για μοιραία. Όταν το backend ανέφερε ότι η ανοδική σύνδεση ομιλίας είχε πέσει, ο client σταματούσε να εμφανίζει ένα μοιραίο σφάλμα και αντ' αυτού επανασυνδεόταν — με σκόπιμα ήρεμο UX: μία κεχριμπαρένια ειδοποίηση «Επανασύνδεση…» (όχι μία ανά προσπάθεια), μια πράσινη ειδοποίηση «Επανασυνδέθηκε» όταν επέστρεφε, και το τελικό σφάλμα «παρακαλώ επανεκκινήστε» μόνο αφού εξαντλούνταν κάθε προσπάθεια.
Και μετά κάτι που μου αρέσει σε αυτή την ομάδα: μία έκδοση αργότερα, στην v1.6.10, διαγράψαμε αυτή τη λογική επανασύνδεσης από την πλευρά του client — περίπου 113 γραμμές — και μετακινήσαμε την επανασύνδεση στο backend, το οποίο τώρα εκπέμπει ήσυχα μηνύματα κατάστασης «επανασύνδεση» που ο client απλώς αντικατοπτρίζει. Χτίσαμε την ανάκτηση στον client, αποφασίσαμε ότι το backend ήταν ο σωστός ιδιοκτήτης, και τη μετακινήσαμε. Αυτό δεν είναι δισταγμός· είναι ο ειλικρινής κύκλος ζωής ενός δύσκολου προβλήματος. Λογική αξιοπιστίας μοιρασμένη και στα δύο άκρα μιας σύνδεσης είναι κακό σημάδι — διάλεξε έναν ιδιοκτήτη. (Το backend τελικά έγινε ο οριστικός ιδιοκτήτης αυτού, που είναι η ιστορία επανασύνδεσης που αφηγείται το γιατί ο AI notetaker σας σταματά σε κακό Wi-Fi.)
Η ίδια έκδοση έκανε τα όρια χρόνου εγγραφής ανθρώπινα: αντί για ένα ωμό σφάλμα, μια προειδοποιητική ειδοποίηση που αφήνει την εγγραφή να συνεχιστεί, και ένα καθαρό μήνυμα «Επιτεύχθηκε το όριο εγγραφής» όταν πραγματικά σταματά — η φιλική πρόταση αναδεικνύεται, το εσωτερικό πρόθεμα αφαιρείται.
Μία επαναπροσπάθεια να τις κυβερνά όλες
Μέχρι την v1.6.11, η ίδια λογική επαναπροσπάθειας-με-backoff είχε αντιγραφεί-επικολληθεί σε τρία διαφορετικά σημεία — αυθεντικοποίηση, ενημερώσεις, αποδόμηση συνεδρίας. Έτσι ενοποιήθηκε σε ένα ενιαίο module με έναν κοινό κανόνα για το τι αξίζει καν να επαναληφθεί: οι παροδικές αποτυχίες δικτύου (επαναφορά σύνδεσης, timeout, αναλαμπή DNS, πτώση socket) επαναλαμβάνονται με εκθετικό backoff· ένα πραγματικό σφάλμα σέρβερ αποτυγχάνει γρήγορα, γιατί η επανάληψη ενός γνήσιου 4xx απλώς σπαταλά τον χρόνο του χρήστη. Ένας ταξινομητής, μερικά ονομασμένα presets, τρεις καλούντες μεταφερμένοι σε αυτό.
Τι μας δίδαξε αυτή η ομάδα
- Ένα ανεπεξέργαστο συμβάν
errorείναι κρασάρισμα, όχι μια γραμμή log. Το Node ξαναρίχνει τα συμβάντα error που δεν έχουν ακροατή. Αφαίρεσε τους ακροατές σε ένα socket που μπορεί να αποδομείς ακόμα, και πρέπει να επανασυνδέσεις έναν no-op απορροφητή σφαλμάτων — αλλιώς μία χαμένη σύνδεση παίρνει όλη την εφαρμογή μαζί της. - Ανανέωσε τα διαπιστευτήρια πριν τη λήξη και αντιδραστικά σε ένα 401 — με ένα mutex και στα δύο. Το προληπτικό σας κρατά συνδεδεμένους· το αντιδραστικό πιάνει τις οριακές περιπτώσεις· το mutex σημαίνει ότι N ταυτόχρονοι καλούντες προκαλούν ακριβώς μία ανανέωση, και ένα ταβάνι επαναπροσπάθειας σημαίνει ότι ένα κακό token δεν μπορεί να κάνει βρόχο για πάντα.
- Επανάλαβε μόνο σε παροδικά σφάλματα, και κεντρικοποίησε τον ταξινομητή. «Αξίζει να το επαναλάβω αυτό;» είναι μία απόφαση που ανήκει σε ένα μέρος. Η επανάληψη ενός πραγματικού σφάλματος είναι απλώς μια πιο αργή αποτυχία.
- Αποφάσισε ποιος κατέχει την επανασύνδεση. Την προσθέσαμε στον client, μετά τη μετακινήσαμε στο backend. Λογική ανάκτησης που ζει και στα δύο άκρα είναι γεννήτρια bugs· δώσε της ένα μόνο σπίτι.
Αυτό είναι το τρίτο κεφάλαιο της ιστορίας αξιοπιστίας που γίνεται GeekBye v2. Για το προηγούμενο κεφάλαιο, δείτε διαγράψαμε 5.000 γραμμές κώδικα ήχου και οι μεταγραφές άρχισαν να εμφανίζονται δύο φορές (v1.6.0)· για το πού κατέληξε τελικά η επανασύνδεση, γιατί ο AI notetaker σας σταματά σε κακό Wi-Fi· και για ολόκληρο το τόξο, η ανατομία της αποστολής λογισμικού στην τελειότητα.