Κρίσιμες εφαρμογές
Στην προηγούμενη δουλεία είχα συμμετάσχει στην υλοποίηση του έργου της αυτοματοποίησης των διοδίων, το 2000 επί εποχής ΤΕΟ. Με πιέζανε να ολοκληρώσω και να εγκαταστήσω το λογισμικό που έτρεχε στην λωρίδα, κατηγοριοποιούσε τα οχήματα, εξέδιδε αποδείξεις διέλευσης με το κατάλληλο τίμημα και επέτρεπε την διαχείριση του ταμείου για κάθε βάρδια και στο σύνολο της ημέρας. Επισκεπτόμουν σχεδόν κάθε μέρα στα διόδια της Ελευσίνας για δοκιμές και ελέγχους.

Προσπαθούσα να επιχειρηματολογήσω, για το πόσο κρίσιμη είναι η εφαρμογή και ότι δεν έπρεπε να «πέφτει» και ότι χρειαζόμουνα περισσότερο χρόνο για ελέγχους. Τότε είχα ακούσει το επιχείρημα «και το Word πέφτει αλλά είναι μια χαρά εφαρμογή».

Το νόημα της ιστορίας. Κρίσιμες εφαρμογές λογισμικού (mission critical). Αυτές που παρακολουθούν και εποπτεύουν-υλοποιούν διαδικασίες. Πρέπει να λειτουργούν σωστά και συνέχεια. Πρέπει να ανιχνεύουν τα προβλήματα και να προσπαθούν να τα αντιμετωπίσουν. Πρέπει να εμφανίζουν επεξηγηματικά μηνύματα λαθών και την απαραίτητη πληροφορία που θα βοηθήσει στην επίλυση του προβλήματος.

Το 1998 μπλέχτηκα με την λιανική αναπτύσσοντας εφαρμογή ταμείου για super market.
Η προδιαγραφή που μου είχε τεθεί ήταν. «Πετάς το POS-ταμειακή από το παράθυρο. Όταν ξανα-ξεκινήσει πρέπει ή να λειτουργεί ή η εφαρμογή να εμφανίσει μήνυμα για το πρόβλημα που εντοπίζει.

Οι νέες μεθοδολογίες [V] ανάπτυξης λογισμικού (agile) δεν απαιτούν προδιαγραφές και ελέγχους, απλά ευαγγελίζονται γρήγορη ολοκλήρωση.

Άντε και να κάνουμε και κανένα έλεγχο. Συνολικός έλεγχος λειτουργικότητας θα γίνει από τους πελάτες. Θα βγάλουμε το Ν+1 patch (όπου Ν οσοδήποτε μεγάλο) και θα λύσουμε το πρόβλημα.

Και η ανάπτυξη λογισμικού καταλήγει να είναι θέμα μάζας και όχι ικανοτήτων. Υπάρχουν άλλωστε πάνω από 1 δις Ινδοί.

Οι εφαρμογές λογισμικού ΠΡΕΠΕΙ να ελέγχουν για πιθανά και απίθανα λάθη. Αυτά μπορεί να είναι από διαίρεση με το μηδέν, έως την αποτυχία επικοινωνίας ανάμεσα σε δύο σημεία κατά την εκτέλεση μια τραπεζικής συναλλαγής.

Σε προηγούμενη ανάρτηση υλοποίησα σε Delphi ένα modbus master. Ο κώδικας, με κάποιες τροποποιήσεις, μπήκε στην παραγωγή. Και σε μια διαδικασία ογκομέτρησης δεξαμενής εμφανίσθηκαν σφάλματα στην επικοινωνίας με το PLC μέσω ΤCP/IP.

Ας δούμε λοιπόν τι συμβαίνει σε μια TCP/IP επικοινωνία ανάμεσα σε δύο σημεία σε ένα διάλογο ερώτησης - απάντησης.
Κώδικας:
function TModbus.ReadHoldingRegisters(RegNo, Blocks: Word; var Data: array of word): Boolean;
 var FC:char;
     i:integer;
     _Blocks:word;
     RefNum,wCount,msg:string;
 begin
//  Προετοιμασία μηνύματος αποστολής
   ...
   msg:=char(MB_IGNORE_UNITID)+FC+RefNum+wCount;

// Αποστολή μηνύματος και λήψη απάντησης

   result:=Send(TrnId + #$00#$00 + #$00#$06 + msg) and
           Receive(Data[0]);

   if result then // επιτυχία
    for i:=Low(data) to High(data)do
     Data[i]:=swap(Data[i]);
 end;

function TModbus.Send(const Request: string):boolean;
 var len:integer;
 begin
   len:=length(Request);
   try
    FSockStream.Write(Request[1],len);
   except // ESocketError
    on E:Exception do
     begin
      Debug('Send Error %s',[E.Message]);
      result:=false;
     end;
   end;
 end;

function TModbus.Receive(var buffer):boolean;
 var Header : array[0..7] of byte;
     len:byte;
 begin
  try
   FSockStream.read(Header,sizeof(Header)); // including UnitId, FC
   case Header[7] of
    mbfReadHoldingRegs : FSockStream.read(len, 1);
    mbfWriteOneReg     : len:=Header[5]-2;
   end;
   FSockStream.read(buffer, len);
  except
   on E:Exception do
    begin
     Debug('Receive Error %s',[E.Message]);
     result:=false;
    end;
  end
 end;
H ReadHoldingRegisters περιμένει ότι υπάρχει ήδη ανοικτό κανάλι TCP/IP επικοινωνίας με τον mobus slave και τον ρωτάει για το περιεχόμενο ενός register.
Σχηματίζει ένα modbus μήνυμα και αν όλα πάνε καλά, το στέλνει, λαμβάνει την απάντηση και την επιστρέφει στον καλούντα.

Η γραμμή
Κώδικας:
   result:=Send(TrnId + #$00#$00 + #$00#$06 + msg) and
           Receive(Data[0]);
είναι τρόπος γραφής κώδικα χωρίς exceptions. Μάλιστα στις Send & Receive λαμβάνεται πρόνοια ώστε αν δημιουργηθούν exceptions αυτά να επιστραφούν σαν function results. Eπιστρέφουμε την επιτυχία / αποτυχία της επικοινωνίας σαν function result και σε επιτυχία και το περιεχόμενο του register.

Η επικοινωνία όμως μπορεί να διακοπεί από την άλλη άκρη για οποιοδήποτε λόγο.
Ο modbus Master «τρέχει» σε φορητό H/Y με backup μπαταρία και το PLC τροφοδοτείται με γεννήτρια. Αν σταματήσει η γεννήτρια τότε ο modbus Master θα λάβει σφάλμα 10054.

Εξαιρέσεις (Exceptions)
Η υποστήριξη των εξαιρέσεων (Exceptions) στις νέες γλώσσες C++, Java, C# και φυσικά Delphi προσφέρουν μια εξαιρετική δυνατότητα στον τρόπο υλοποίησης της λογικής των προγραμμάτων.

Φανταστείτε τις εξαιρέσεις σαν διαρροές ρεύματος σε ένα κύκλωμα που μόλις συμβούν το ρελέ διαρροής τις ανιχνεύει και τις χειρίζεται. Φυσικά η συσκευή που προκάλεσε την διαρροή παύει να λειτουργεί. Μπορούμε να έχουμε πολλούς τύπων διαρροών και πολλά επίπεδα από ρελέ διαρροής.

Κάθε εφαρμογή έχει ένα default διαχειριστή εξαιρέσεων που «πιάνει» την εξαίρεση και την χειρίζεται κατάλληλα. Εξαιρέσεις παράγονται από το hardware (σφάλματα προσπέλασης μνήμης, αριθμητικός επεξεργαστής) ή σαν αποτέλεσμα εντολών μετά από λογικούς ελέγχους. Μπορούμε να χειριστούμε εξαιρέσεις με το try / except end και να δημιουργήσουμε εξαιρέσεις με το raise Exception.Create.

Χωρίς χρήση εξαιρέσεων γράφουμε ελέγχοντας πάντα για σφάλματα μέσω του αποτελέσματος της ρουτίνας που καλούμε.
Κώδικας:
  if PLC.ReadHoldingRegisters(RegNo, Blocks, wData) then begin
    debug('Read Holding Registers value(s) read:');
    for i := 0 to pred(Blocks) do
     debugfmt('%02.2d:%04.4x',[RegNo + i,wData[i]]);
    end
  else
    debug('PLC ReadHoldingRegisters operation failed!');
Με χρήση εξαιρέσεων γράφουμε ΑΓΝΟΩΝΤΑΣ τα σφάλματα.

Εάν στην ReadHoldingRegisters δημιουργηθεί εξαίρεση τότε ο έλεγχος μεταφέρεται στο except block.
Κώδικας:
  try
   PLC.ReadHoldingRegisters(RegNo, Blocks, wData);
   debug('Read Holding Registers value(s) read:');
   for i := 0 to pred(Blocks) do
    debugfmt('%02.2d:%04.4x',[RegNo + i,wData[i]]);
  except
   on E:Exception do 
    Debug(' ReadHoldingRegisters failed %s',[E.Message]);
  end;
Τι πρέπει να κάνουμε ώστε να χειρισθούμε πιθανά σφάλματα επικοινωνίας

1. Να πιάσουμε τις πιθανές εξαιρέσεις
2. Να επαναλάβουμε την διαδικασία αφού ξανασυνδεθούμε με τον modbus client
3. Σε περίπτωση αδυναμίας σύνδεσης με τον modbus client να αφήσουμε την εξαίρεση που παράγεται να ενημερώσει αυτόν που μας κάλεσε.

Κώδικας:
function TModbus.ReadHoldingRegisters(RegNo, Blocks: Word; var Data: array of word): Boolean;
   ...
// Αποστολή μηνύματος και λήψη απάντησης
   retry:=0;
   while true do
    try
     // σε σφάλμα ο ελεγχος  μεταφέρεται στο exception handler
     Send(TrnId + #$00#$00 + #$00#$06 + msg);
     // σε σφάλμα ο ελεγχος  μεταφέρεται στο exception handler
     Receive(Data[0]);

     // κανένα σφάλμα
     for i:=Low(data) to High(data)do Data[i]:=swap(Data[i]);

     break; // the while loop
    except
     Debug('ReadHoldingRegisters Error %s',
           [Exception(ExceptingObject).Message]);     
     // Διέκοψε την επικοινωνία, δεν δημιουργεί exceptions
     Active:=false;
     // Συνδέσου ξανά
     // μπορεί να δημιουργήσει exception αλλά η αδυναμία σύνδεσης είναι
     // μη διαχειρίσημη. Να ενημερώσουμε τον caller.     
     Active:=true;
     inc(retry);
     // αν έχουμε υπερβεί τις προσπάθειες ξαναδημιούργησε 
     // την εξαίρεση ώστε να την χειρισθεί ο έξω κόσμος.
     if retry>2 then raise
    end;
  end;

function TModbus.Send(const Request: string):boolean;
 var len:integer;
 begin
   len:=length(Request);
   // δημιουργεί εξαιρέσεις σε περίπτωση σφάλματος
   FSockStream.Write(Request[1],len);
 end;

function TModbus.Receive(var buffer):boolean;
 var Header : array[0..7] of byte;
     len:byte;
 begin
   // δημιουργεί εξαιρέσεις σε περίπτωση σφάλματος
  FSockStream.read(Header,sizeof(Header)); // including UnitId, FC
  case Header[7] of
    mbfReadHoldingRegs : FSockStream.read(len, 1);
    mbfWriteOneReg     : len:=Header[5]-2;
  end;
   // δημιουργεί εξαιρέσεις σε περίπτωση σφάλματος
  FSockStream.read(buffer, len);
 end;
Μηχανισμός πίσω από τις εξαιρέσεις
Έστω ότι είμαστε σε μία ρουτίνα σε βάθος κλήσης 2. Αυτό σημαίνει ότι στο stack έχουν δημιουργηθεί τα stack frames ώστε με την ολοκλήρωση κάθε ρουτίνας να επιστρέψουμε σε όποια ρουτίνα μας έχει καλέσει.
Κώδικας:
MainProgram
Begin
 Try  
  LevelA;
 Except 
  …
 end
End;

Procedure LevelA;
Begin
 LevelB;
 LevelA_next_instruction
End;

Procedure LevelB;
Begin
 LevelB_1st_instruction
 Raise Exception.Create;
 LevelB_2nd_instruction
end
Η δημιουργία εξαίρεσης στην ρουτίνα LevelB θα αποτρέψει την εκτέλεση της γραμμής LevelB_2nd_instruction. Η ροή του προγράμματος να συνεχίσει στο except block του mainProgram. Για να συμβεί αυτό θα πρέπει να αφαιρεθεί το stack frame που δημιουργήθηκε κατά την κλήση από την LevelA στην LevelB και το αντίστοιχο της LevelA. Γενικά η διαδικασία ξεδιπλώματος (unwind) του stack έχει κόστος. Οι εξαιρέσεις είναι μηχανισμός για διακοπή της εκτέλεσης του προγράμματος σε περίπτωση σφάλματος.

Τα σφάλματα πρέπει να είναι σπάνια και έτσι το κόστος χειρισμού τους μπορεί να είναι υψηλό. Στο Delphi υπάρχει η σιωπηλή εξαίρεση ΕΑbort. Σταματάει την ροή του προγράμματος χωρίς μήνυμα. ΜΗΝ χρησιμοποιείτε exceptions για να κατευθύνετε την κανονική ροή λειτουργίας του προγράμματος γιατί είναι πολύ αργή σαν διαδικασία.

Constructors & Exceptions
Αν στον constructor ενός object δημιουργηθεί εξαίρεση τότε η δημιουργία του object ΑΠΟΤΥΓΧΑΝΕΙ. Φυσικά δεν γίνεται ανάθεση τιμής στη μεταβλητή του object και η ροή του προγράμματος μεταφέρεται στο επόμενο exception handler.

Ο τρόπος με τον οποίο διαχειριζόμαστε την «ζωή» περισσοτέρων από ενός objects είναι ο ακόλουθος.
Κώδικας:
var a,b:TObject;

begin
 a:=nil;
 b:=nil;
 try
  try
   a:=TObject.Create;
   b:=TObject.Create;

  // use a, b
  except
   on E:Exception do // handle the exception
  end;
 finally
  b.Free;
  a.Free
 end;
end;
Σε περίπτωση εξαίρεσης (out of memory) στην δημιουργία του object a, ο έλεγχος του προγράμματος θα μεταφερθεί αρχικά στο exception block και στην συνέχεια στο finally block. Εκεί το a=nil και η ΤObject.Free το ελέγχει.

Αντίστοιχα, αν «σκάσει» η δημιουργία του object b στο finally θα γίνει free to a και το b=nil.

Συνολική διαχείριση εξαιρέσεων
Κώδικας:
procedure TForm1.FormCreate(Sender: TObject);
begin
..
  Application.OnException:=HandleException;
...
end;

procedure TForm1.HandleException(Sender: TObject; E: Exception);
 begin
  debugFmt('HandleException %s',[e.message]);
 end;
Σε αυτή την περίπτωση θα ήταν χρήσιμο να απενεργοποιήσετε την δυνατότητα Break On Exceptions του Delphi IDE.

TCP/IP error codes
http://frontier.userland.com/stories/storyReader$173

Delphi Exception
http://www.delphibasics.co.uk/Articl...ame=Exceptions
http://edn.embarcadero.com/article/30115
http://conferences.embarcadero.com/article/32156

JclDebug
http://pilif.github.com/2002/12/jcldebug/
http://sourceforge.net/projects/jcl/

Win32 Exception handling
http://www.godevtool.com/ExceptFrame.htm
http://www.microsoft.com/msj/0197/ex...exception.aspx
http://www.drbob42.com/cbuilder/structur.htm