Τεχνικό Άρθρο

Αποθηκεύσεις ανθεκτικές σε κατάρρευση στο HotXLS: προσωρινά αρχεία σταδίου σε Delphi

Μια αποθήκευση που πεθαίνει στα μισά, είτε από αναγκαστική επανεκκίνηση, είτε από μια σκοτωμένη διεργασία, είτε από έναν δίσκο που γεμίζει στη μέση της εγγραφής, παραδοσιακά σήμαινε ένα πράγμα για μια μορφή χτισμένη γύρω από εγγραφές επί τόπου: όποια byte έφτασαν στον δίσκο πριν τη διακοπή είναι αυτό που παίρνετε πίσω, και ένα κομμένο βιβλίο εργασίας δεν ανοίγει ξανά. Το HotXLS κλείνει αυτή τη λειτουργία αποτυχίας με μια διαδρομή αποθήκευσης ανθεκτική σε κατάρρευση που χρησιμοποιείται για κάθε αρχείο XLSX, ODS, και κλασικό XLS που γράφει. Κάθε κλήση SaveAs γράφει το πλήρες νέο αρχείο σε ένα προσωρινό αρχείο που δημιουργείται δίπλα στον προορισμό, έπειτα το δεσμεύει με μία μοναδική ατομική μετονομασία MoveFileExW από το API των Windows, οπότε μια διακοπείσα αποθήκευση μπορεί μόνο να αποτύχει να παράγει το νέο αρχείο, ποτέ δεν βλάπτει αυτό που ήδη είχατε. Η ίδια πειθαρχία σταδίου-έπειτα-ανταλλαγής εκτελείται ομοιόμορφα σε αμφότερες τις μηχανές αποθήκευσης του HotXLS, τον γραφέα BIFF8 πίσω από το κλασικό XLS και τον γραφέα OOXML πίσω από το XLSX και το ODS, και είναι ένα μοτίβο που αξίζει να δανειστείτε για οποιοδήποτε αρχείο αντικαθιστά απευθείας ο δικός σας κώδικας Delphi, υπολογιστικά φύλλα ή όχι

Τι συμβαίνει αν διακοπεί μια αποθήκευση βιβλίου εργασίας στα μισά;

Η άμεση απάντηση είναι ότι εξαρτάται εντελώς από το πώς αγγίζει ο γραφέας το αρχείο προορισμού, και η κοινή υλοποίηση, το άνοιγμα του αρχείου στόχου και η ροή νέου περιεχομένου απευθείας μέσα του, είναι εντάξει για όσο καιρό δεν πάει ποτέ κάτι στραβά. Τη στιγμή που κάτι πάει, μια κατάρρευση, ένα αναγκαστικό σκότωμα διεργασίας, ένας κοινόχρηστος φάκελος δικτύου που πέφτει στη μέση της εγγραφής, το αρχείο στον δίσκο μένει σε όποια ενδιάμεση κατάσταση είχε φτάσει ο γραφέας: έναν κεντρικό κατάλογο ZIP που ποτέ δεν προσαρτήθηκε για XLSX ή ODS, ή μια ροή BIFF που της λείπουν εγγραφές που αναμένει ένας αναγνώστης για κλασικό XLS. Το Excel δεν το επιδιορθώνει ομαλά αυτό, ούτε κανένας άλλος καταναλωτής που αναμένει ένα πλήρες αρχείο, οπότε το πρακτικό αποτέλεσμα είναι ένα βιβλίο εργασίας που άνοιξε μια χαρά χθες και αρνείται να ανοίξει σήμερα

Πώς το HotXLS στήνει κάθε αποθήκευση πίσω από μία ατομική ανταλλαγή

Το HotXLS ποτέ δεν ανοίγει το αρχείο προορισμού για εγγραφή απευθείας, για καμία από τις τρεις μορφές που αποθηκεύει. Η ακολουθία είναι το ίδιο σχήμα κάθε φορά: χτίστε την πλήρη έξοδο κάπου που δεν είναι το αρχείο που ήδη έχει ο χρήστης στον δίσκο, και μετακινήστε την στη θέση της μόνο μόλις εκείνο το χτίσιμο έχει πλήρως πετύχει. Συγκεκριμένα, το SaveAs δημιουργεί ένα κενό προσωρινό αρχείο στον ίδιο φάκελο με τη διαδρομή στόχο, γράφει ολόκληρο το νέο βιβλίο εργασίας σε εκείνο το προσωρινό αρχείο, και μόνο αφού εκείνη η εγγραφή επιστρέψει χωρίς σφάλμα δεσμεύει το προσωρινό αρχείο πάνω από τον προορισμό με μία μοναδική μετονομασία. Τίποτα από αυτά δεν απαιτεί ιδιότητα προς ενεργοποίηση· είναι απλώς αυτό που κάνει το SaveAs για μια απλή διαδρομή αρχείου, σε κάθε κλήση

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Report');
    Sheet.Cells[1, 1].Value := 'Nothing special to enable here';
    // If this call is interrupted, monthly-report.xlsx on disk stays
    // either the old version, complete, or the new version, complete
    if Book.SaveAs('monthly-report.xlsx', xlsxOpenXMLWorkbook) <> 1 then
      raise Exception.Create('Save failed, see Book.LastDiagnostic');
  finally
    Book.Free;
  end;
end;

Η ίδια πειθαρχία εφαρμόζεται στον κλασικό γραφέα XLS, όχι μόνο στον OOXML, και τα δύο προσωρινά αρχεία μοιράζονται μάλιστα μια σύμβαση ονομασίας: και τα δύο καλούν το API GetTempFileNameW των Windows με το πρόθεμα hxl, οπότε μια αποθήκευση διακοπείσα πριν τον καθαρισμό μπορεί να αφήσει πίσω ένα περιπλανώμενο αρχείο με όνομα όπως hxl4C2A.tmp δίπλα στο βιβλίο εργασίας σας. Εκείνο το αρχείο δεν είναι αλλοίωση, είναι απόδειξη ότι ο μηχανισμός λειτούργησε ακριβώς όπως σχεδιάστηκε: η ελλιπής εγγραφή σταμάτησε εκεί, και το πραγματικό σας βιβλίο εργασίας ποτέ δεν ανοίχτηκε καν για εγγραφή εξαρχής. Η θέα ενός τέτοιου αρχείου μετά από κατάρρευση είναι ασφαλές να διαγραφεί και τίποτα προς διερεύνηση

Γιατί να στήνεται το προσωρινό αρχείο δίπλα στο βιβλίο εργασίας αντί στο %TEMP%;

Η σύντομη απάντηση είναι ότι η μετονομασία του MoveFileExW είναι ατομική μόνο όταν η πηγή και ο προορισμός κάθονται στον ίδιο τόμο, και ο πιο σίγουρος τρόπος να το εγγυηθείτε αυτό χωρίς να ζητήσετε από τον καλούντα να ρυθμίσει οτιδήποτε είναι να προκύψει η θέση του προσωρινού αρχείου από την ίδια τη διαδρομή προορισμού. Το HotXLS υπολογίζει τον δικό του φάκελο του στόχου και δίνει εκείνον τον κατάλογο απευθείας στο GetTempFileNameW, οπότε το προσωρινό αρχείο δημιουργείται πάντα στον ίδιο δίσκο, τον ίδιο τόμο, με το αρχείο που πρόκειται να αντικαταστήσει, αυτόματα, σε κάθε αποθήκευση. Αν η βιβλιοθήκη αντ' αυτού έστηνε εγγραφές στον προσωρινό φάκελο του συστήματος, μια διαδρομή στόχος σε διαφορετικό δίσκο ή έναν χαρτογραφημένο τόμο δικτύου θα μετέτρεπε το τελικό βήμα σε μια λειτουργία μεταξύ τόμων, την οποία το API των Windows είτε αρνείται εντελώς είτε, αν ένας καλών ρητά επιλέξει με μια επιπλέον σημαία που το HotXLS δεν ορίζει εδώ, υποβαθμίζεται σιωπηλά σε ένα μη ατομικό αντίγραφο ακολουθούμενο από διαγραφή, ανοίγοντας ξανά ακριβώς το παράθυρο διακοπής που υπάρχει όλος αυτός ο μηχανισμός για να κλείσει

Το βήμα δέσμευσης: MoveFileExW, write-through, και τι συμβαίνει σε αποτυχία

Το τελικό βήμα κάθε αποθήκευσης είναι ακριβώς μία κλήση API των Windows, το MoveFileExW, φέρον δύο σημαίες που κάνει η καθεμία ξεχωριστή δουλειά. Το MOVEFILE_REPLACE_EXISTING είναι αυτό που επιτρέπει στη μετονομασία να προσγειωθεί σε ένα αρχείο που ήδη υπάρχει· χωρίς αυτό, μια μετονομασία που στοχεύει μια υπάρχουσα διαδρομή απλώς αποτυγχάνει, κάτι που θα ματαίωνε ολόκληρο το νόημα μιας αποθήκευσης που προορίζεται να αντικαταστήσει ένα βιβλίο εργασίας που ήδη έχετε. Το MOVEFILE_WRITE_THROUGH καλύπτει την ανθεκτικότητα: λέει στη συνάρτηση να μην επιστρέψει μέχρι η μετακίνηση να έχει πράγματι ολοκληρωθεί στον δίσκο, αντί να επιστρέφει μόλις η μετονομασία απλώς μπει σε ουρά, κλείνοντας μια στενότερη αλλά πραγματική συνθήκη αγώνα όπου μια κατάρρευση αμέσως μετά την επιστροφή του SaveAs θα μπορούσε ακόμη να πιάσει την ανταλλαγή εν πτήσει. Αν το προσωρινό αρχείο δεν μπορεί να δημιουργηθεί, ή η τελική μετονομασία αποτυγχάνει για οποιονδήποτε λόγο (πρόβλημα άδειας, κλειδωμένος προορισμός, ασυμφωνία τόμου), το HotXLS διαγράφει το ίδιο το προσωρινό αρχείο αντί να αφήνει σκουπίδια πίσω, και το αρχείο προορισμού μένει ακριβώς όπως ήταν πριν την κλήση

Result := Book.SaveAs(TargetPath, xlsxOpenXMLWorkbook);
if Result <> 1 then
begin
  // TargetPath on disk is unchanged; safe to retry, alert, or
  // fall back to a different path without touching prior output
  LogWriter.Write(Format('SaveAs failed (%d): %s',
    [Book.LastDiagnostic.Code, Book.LastDiagnostic.Message]));
  Exit(False);
end;

Το ίδιο το SaveAs κρατά τη σύμβαση επιστροφής κοινή σε όλο το HotXLS, ένα σε επιτυχία, αρνητικό αριθμό σε αποτυχία, αλλά ένας γυμνός ακέραιος δεν λέει γιατί απέτυχε μια αποθήκευση, και το να αντιμετωπίζετε κάθε αρνητικό αποτέλεσμα με τον ίδιο τρόπο πετά μακριά πληροφορία που μια πολιτική επανάληψης θα μπορούσε πράγματι να χρησιμοποιήσει. Η ιδιότητα LastDiagnostic, και η πληρέστερη συλλογή Diagnostics πίσω της, φέρει το μήνυμα που δημιούργησε εσωτερικά το HotXLS, διακρίνοντας ένα προσωρινό αρχείο που δεν μπόρεσε να δημιουργηθεί από μια μετονομασία που αρνήθηκαν τα Windows. Μια εργασία παρτίδας που καταγράφει το Code και το Message σε κάθε αποτυχημένο SaveAs χτίζει ακριβώς τα στοιχεία που θέλετε τη μία φορά που ένας πελάτης αναφέρει μια αποθήκευση που σιωπηλά δεν έκανε τίποτα

Το κλασικό XLS πληρώνει με μνήμη, το XLSX και το ODS πληρώνουν με δίσκο

Οι δύο μηχανές αποθήκευσης φτάνουν στο ίδιο αποτέλεσμα ανθεκτικό σε κατάρρευση με διαφορετικές διαδρομές, και η διαφορά έχει σημασία αν ήδη συντονίζετε είτε τη μία είτε την άλλη για μια μεγάλη εργασία παρτίδας. Ο κλασικός γραφέας XLS χτίζει ολόκληρο το σύνθετο έγγραφο OLE στη μνήμη πρώτα, χρησιμοποιώντας δομημένη αποθήκευση υποστηριζόμενη από χειριστή μνήμης, και αντιγράφει μόνο εκείνο το ολοκληρωμένο buffer έξω στο αδελφό προσωρινό αρχείο σε μία εγγραφή· το σκεπτικό στη δική του πηγή του HotXLS είναι άμεσο: το χτίσιμο ολόκληρου του αρχείου στη μνήμη πρώτα είναι αυτό που εμποδίζει μια αποτυχημένη ή ακυρωμένη αποθήκευση από το να κόψει ποτέ τον προορισμό. Ο γραφέας XLSX και ODS αντ' αυτού ρέει τις καταχωρίσεις ZIP του στο προσωρινό αρχείο καθώς παράγονται, το ίδιο στήσιμο σε επίπεδο αρχείου με διαφορετικό προφίλ μνήμης. Αν ήδη βασίζεστε στο StreamingWrite για να κρατήσετε μεγάλες εξαγωγές XLSX μέσα στο όριο μνήμης ενός container, γνωρίζετε ότι ο αντίστοιχος μοχλός για εξαγωγή κλασικού XLS δεν υπάρχει με την ίδια μορφή: η εγγύηση ανθεκτικότητας σε κατάρρευση είναι άνευ όρων και στις δύο περιπτώσεις, αλλά μια πολύ μεγάλη παλαιού τύπου εξαγωγή .xls κρατά την πλήρη έξοδό της στη RAM ούτως ή άλλως, ένας συμβιβασμός που καλύπτεται σε μεγαλύτερο βάθος στο άρθρο μας για ροές εγγραφής σε εργασίες παρτίδας διακομιστή

Εφαρμόζοντας το ίδιο μοτίβο έξω από το HotXLS, και πού τελειώνει η εγγύηση

Ο δανεισμός του μοτίβου είναι κυρίως ζήτημα καλωδίωσης των ίδιων δύο κλήσεων API των Windows στις οποίες βασίζεται εσωτερικά το HotXLS. Το GetTempFileNameW σας δίνει ένα μοναδικά ονομασμένο, κενό αρχείο σε έναν φάκελο της επιλογής σας, και το MoveFileExW δεσμεύει την ολοκληρωμένη σας εγγραφή πάνω από τον πραγματικό προορισμό σε ένα βήμα· μια ελάχιστη έκδοση της ίδιας ρουτίνας που εκτελεί το HotXLS πριν από κάθε SaveAs μοιάζει έτσι

function SaveFileAtomically(const Path: WideString; const Contents: TBytes): Boolean;
var
  Dir, TempName: WideString;
  Buffer: array[0..MAX_PATH] of WideChar;
  FS: TFileStream;
begin
  Result := False;
  Dir := ExtractFilePath(ExpandFileName(Path));
  FillChar(Buffer, SizeOf(Buffer), 0);
  if GetTempFileNameW(PWideChar(Dir), 'app', 0, @Buffer[0]) = 0 then
    Exit;
  TempName := PWideChar(@Buffer[0]);
  try
    FS := TFileStream.Create(TempName, fmCreate or fmShareExclusive);
    try
      FS.WriteBuffer(Contents[0], Length(Contents));
    finally
      FS.Free;
    end;
    Result := MoveFileExW(PWideChar(TempName), PWideChar(ExpandFileName(Path)),
      MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH);
  finally
    if not Result then
      DeleteFileW(PWideChar(TempName));
  end;
end;

Η εγγύηση έχει πραγματικά όρια που αξίζει να γνωρίζετε πριν βασιστείτε σε αυτήν τυφλά. Το στήσιμο ενός πλήρους αντιγράφου πριν την αντικατάσταση του πρωτοτύπου σημαίνει ότι μια αποθήκευση χρειάζεται στιγμιαία χώρο δίσκου τόσο για το παλιό αρχείο όσο και για το νέο, περίπου διπλάσιο από το μέγεθος του βιβλίου εργασίας για τη διάρκεια της εγγραφής, κάτι που είναι εντάξει για μια αναφορά και αξίζει να ελεγχθεί για μια εξαγωγή πολλών gigabyte που τρέχει έναντι ενός σχεδόν γεμάτου τόμου. Το προσωρινό αρχείο πρέπει επίσης να προσγειωθεί στον ίδιο φάκελο με τον προορισμό, οπότε όποιος λογαριασμός τρέχει το HotXLS χρειάζεται άδεια δημιουργίας-αρχείου σε εκείνον τον φάκελο συγκεκριμένα, όχι απλώς άδεια αντικατάστασης του ενός αρχείου που ήδη γνωρίζει· μια ανάπτυξη που κλειδώνει έναν φάκελο προορισμού σε επί τόπου επεξεργασίες συγκεκριμένων υπαρχόντων ονομάτων αρχείων, αντί για πρόσβαση εγγραφής σε επίπεδο φακέλου, θα δει το SaveAs να αποτυγχάνει στο βήμα προσωρινού αρχείου παρότι η ισοδύναμη άμεση εγγραφή θα είχε πετύχει

Δύο ακόμη όρια αξίζει να σημειωθούν καθαρά. Ένας προορισμός σε κοινόχρηστο φάκελο δικτύου ή μέσα σε έναν φάκελο συγχρονισμένο από το OneDrive ή έναν παρόμοιο πελάτη μπορεί να συμπεριφέρεται διαφορετικά από το τοπικό NTFS ακόμη και αν τα Windows εξακολουθούν να το αναφέρουν ως έναν μοναδικό τόμο, αφού ο οδηγός συστήματος αρχείων μπροστά του μπορεί να μην υλοποιεί τη μετονομασία με τον ίδιο τρόπο· αν ο στόχος ανάπτυξής σας αποθηκεύει μέσω διαδρομής δικτύου, αξίζει να δοκιμάσετε μια αναγκαστική διακοπή εκεί συγκεκριμένα αντί να υποθέτετε ότι η συμπεριφορά τοπικού δίσκου μεταφέρεται. Και ολόκληρος ο μηχανισμός περιορίζεται στην αποθήκευση σε ονομασμένο αρχείο. Καλέστε το SaveAs έναντι ενός TStream αντ' αυτού, και το HotXLS γράφει απευθείας σε όποια ροή του δώσατε, χωρίς αρχείο προορισμού προς στήσιμο ή προστασία, επειδή η ανθεκτικότητα εκείνης της ροής (ένα buffer μνήμης, ένα ανέβασμα δικτύου, ένα blob βάσης δεδομένων) είναι εξ ολοκλήρου ευθύνη του δικού σας κώδικα από εκείνο το σημείο και μετά

Ένα πέρασμα επαλήθευσης μπορεί να βασιστεί ακριβώς σε αυτή την εγγύηση μετά, συμπεριλαμβανομένου του είδους που είναι ενσωματωμένο σε έναν πάγκο εργασίας ελέγχου και μετατροπής βιβλίου εργασίας: ένα επανανοιγμένο αρχείο που επιστρέφει κοντό ή απόν είναι ένα πραγματικό πρόβλημα μετατροπής προς διερεύνηση, ποτέ μια αποθήκευση που διακόπηκε στα μισά και άφησε κάτι διφορούμενο στον δίσκο. Οι εγγραφές σταδίου ανθεκτικές σε κατάρρευση είναι ενσωματωμένες στο SaveAs για κάθε βιβλίο εργασίας XLSX, ODS, και κλασικό XLS που παράγει το εξάρτημα HotXLS για Delphi και C++Builder, χωρίς να απαιτείται ρύθμιση για την ενεργοποίησή της