Écrit par

Solution Architect at Zorgi
MOD
Article Lorenzo Scalese · Mars 6 9m read

Traitement des erreurs et nettoyage hautement efficace dans ObjectScript

Introduction et Motivation

Une unité de code ObjectScript (une méthode de classe, par exemple) peut produire divers effets secondaires inattendus en interagissant avec des parties du système en dehors de son propre champ d'application sans effectuer de nettoyage approprié. Voici une liste non exhaustive de ces effets:

  • Transactions
  • Verrous
  • Périphériques d'E/S
  • Curseurs SQL
  • Indicateurs et paramètres système
  • $Namespace
  • Fichiers temporaires

L'utilisation de ces fonctionnalités linguistiques importantes sans nettoyage approprié et sans codage défensif peut entraîner le dysfonctionnement d'une application normalement correcte, de manière inattendue et difficile à déboguer. Il est essentiel que le code de nettoyage se comporte correctement dans tous les cas d'erreur possibles, d'autant plus que les cas d'erreur sont susceptibles d'être ignorés lors des tests superficiels. Cet article décrit en détail plusieurs pièges connus et explique deux modèles visant à assurer une gestion et un nettoyage robustes des erreurs.

Publicité éhontée semi-connexe: vous voulez être sûr de tester tous vos cas limites? Découvrez mon outil de couverture de test sur Open Exchange!

Remarque : j'ai initialement publié ce contenu au sein d'InterSystems en juin 2018. Le publier sur la communauté des développeurs figurait sur ma liste de choses à faire depuis un an et demi. Vous savez ce qu'on dit...

Pièges à éviter

Transactions

Une approche naturelle et simpliste des transactions consiste à les encapsuler dans un bloc try/catch, avec un TRollback dans le catch, comme suit:

Try {
    TSTART
    // ... faites de trucs avec vos données...
    TCOMMIT
} Catch e {
    TROLLBACK
    // e.AsStatus(), e.Log(), etc.
}

Dans l'absolu, à condition que le code entre TStart et TCommit génère des exceptions plutôt que d'émettre Quit prématurément en cas d'erreur, ce code est parfaitement valide. Cependant, il est risqué pour deux raisons:

  • Si un autre développeur ajoute un "Quit" dans le bloc Try, une transaction restera ouverte. Une telle modification serait facile à manquer lors de la révision du code, surtout s'il n'est pas évident que des transactions sont impliquées dans le contexte actuel.
  • Si la méthode avec ce bloc est appelée depuis une transaction externe, TRollback annulera tous les niveaux de transaction.

Une meilleure approche consiste à suivre le niveau de transaction au début de la méthode et à revenir à ce niveau de transaction à la fin. Par exemple:

Set tInitTLevel = $TLevel
Try {
    TSTART
    // ... faites de trucs avec vos données ...
    // Ce qui suit va bien maintenant; il n'est pas nécessaire de lancer tStatus comme exception.
    If $$$ISERR(tStatus) {
        Quit
    }
    // ... Faites  plus de trucs avec vos données ...
    TCOMMIT
} Catch e {
    // e.AsStatus(), e.Log(), etc.
}
While $TLevel > tInitTLevel {
    // Il suffit de revenir en arrière d'un niveau de transaction à la fois
    TROLLBACK 1
}

Locks

Tout code utilisant des verrous incrémentiels doit également prévoir de décrémenter les verrous dans le code de nettoyage lorsqu'ils ne sont plus nécessaires; sinon, ces verrous seront maintenus jusqu'à la fin du processus. Les verrous ne doivent pas s'échapper en dehors d'une méthode, sauf si l'obtention d'un tel verrou est un effet secondaire documenté de la méthode.

I/O Devices

De même, les modifications apportées au périphérique d'E/S actuel (la variable spéciale $io) ne doivent pas être transmises en dehors d'une méthode, sauf si l'objectif de cette méthode est de modifier le périphérique actuel (par exemple, pour activer la redirection d'E/S). Lorsque vous travaillez avec des fichiers, il est préférable d'utiliser le package %Stream plutôt que les fichiers séquentiels directs d'E/S à l'aide de OPEN / USE / READ / CLOSE. Dans les autres cas, où les périphériques d'E/S doivent être utilisés, il convient de veiller à restaurer le périphérique d'origine à la fin de la méthode. Par exemple, le modèle de code suivant est risqué:

Method ReadFromDevice(pSomeOtherDevice As %String)
{
    Open pSomeOtherDevice:10
    Use pSomeOtherDevice
    Read x
    // ... faites des choses compliquées avec X ...
    Close pSomeOtherDevice
}

Si une exception est déclenchée avant la fermeture de pSomeOtherDevice, alors $io restera défini comme pSomeOtherDevice, ce qui risque d'entraîner des défaillances en cascade. De plus, lorsque le périphérique est fermé, $io est réinitialisé au périphérique par défaut du processus, qui peut ne pas être le même que le périphérique utilisé avant l'appel de la méthode.

Curseurs SQL

Lorsque vous utilisez un SQL basé sur un curseur, ce dernier doit être fermé en cas d'erreur. Si vous ne fermez pas le curseur cela peut entraîner des fuites de ressources (selon la documentation). De plus, dans certains cas, si vous exécutez à nouveau le code et essayez d'ouvrir le curseur, vous obtiendrez une erreur "déjà ouvert" (already open) (SQLCODE -101).

Indicateurs et paramètres système

Dans de rares cas, le code de l'application peut nécessiter la modification d'indicateurs au niveau du processus ou du système. Par exemple, bon nombre d'entre eux sont définis dans %SYSTEM.Process et %SYSTEM.SQL. Dans tous ces cas, il convient de prendre soin de stocker la valeur initiale et de la restaurer à la fin de la méthode.

$Namespace

Le code qui modifie l'espace de noms doit toujours utiliser New $Namespace au début afin de garantir que les modifications de l'espace de noms ne s'étendent pas au-delà de la portée de la méthode.

Fichiers temporaires

Le code d'application créant des fichiers temporaires, tel que %Library.File:TempFilename (qui, notamment dans InterSystems IRIS, crée physiquement le fichier), doit veiller à supprimer également les fichiers temporaires lorsqu'ils ne sont plus nécessaires.

Modèle recommandé : Try-Catch (-Finally)

De nombreux langages disposent d'une fonctionnalité permettant à une structure try/catch de comporter également un bloc "finally" qui s'exécute une fois le try/catch terminé, qu'une exception se soit produite ou non. ObjectScript ne dispose pas de cette fonctionnalité, mais il est possible de s'en approcher. Voici un modèle général illustrant plusieurs cas problématiques possibles:

ClassMethod MyRobustMethod(pFile As %String = "C:\foo\bar.txt") As %Status
{
    Set tSC = $$$OK
    Set tInitialTLevel = $TLevel
    Set tMyGlobalLocked = 0
    Set tDevice = $io
    Set tFileOpen = 0
    Set tCursorOpen = 0
    Set tOldSystemFlagValue = ""
 
    Try {
        // Verrouillez un élément global, à condition qu'un verrouillage puisse être obtenu dans les 5 secondes.
        Lock +^MyGlobal(42):5
        If '$Test {
            $$$ThrowStatus($$$ERROR($$$GeneralError,"Couldn't lock ^MyGlobal(42)."))
        }
        Set tMyGlobalLocked = 1
         
        // Ouvrez un fichier
        Open pFile:"WNS":10
        If '$Test {
            $$$ThrowStatus($$$ERROR($$$GeneralError,"Couldn't open file "_pFile))
        }
        Set tFileOpen = 1
         
        // [ curseur MyCursor declaré ]
        &;SQL(OPEN MyCursor)
        Set tCursorOpen = 1
         
        // Définissez un indicateur système pour ce processus.
        Set tOldSystemFlagValue = $System.Process.SetZEOF(1)
         
        // Faites ce qui est important...
        Use tFile
        
        TSTART
        
        // [ ... beaucoup de codes importants et complexes modifiant les données ici ... ]
         
        // Et voilà, c'est terminé!
         
        TCOMMIT
    } Catch e {
        Set tSC = e.AsStatus()
    }
     
    // Finally {
 
    // Nettoyage: indicateur système
    If (tOldSystemFlagValue '= "") {
        Do $System.Process.SetZEOF(tOldSystemFlagValue)
    }
     
    // Nettoyage: périphérique
    If tFileOpen {
        Close pFile
        // Si pFile est le périphérique actuel, la commande CLOSE rétablit $io sur le périphérique par défaut du processus,
        // qui peut ne pas être identique à la valeur de $io au moment où la méthode a été appelée.
        // Pour être sûr à 100 %:
        Use tDevice
    }
     
    // Nettoyage: verrous
    If tMyGlobalLocked {
        Lock -^MyGlobal(42)
    }
     
    // Nettoyage: transactions
    // Revenez un niveau à la fois jusqu'à notre niveau de transaction initial.
    While $TLevel > tInitialTLevel {
        TROLLBACK 1
    }
     
    // } // fin "finally"
    Quit tSC
}

Remarque : dans cette approche, il est essentiel d'utiliser "Quit" au lieu de "Return" dans le bloc Try ... ; "Return" contournera le nettoyage.

Modèle recommandé: objets et destructeurs enregistrés

Parfois, le code de nettoyage peut devenir complexe. Dans ce cas, il peut être judicieux de faciliter la réutilisation du code de nettoyage en l'encapsulant dans un objet enregistré. L'état du système est suivi lorsque l'objet est initialisé ou lorsque les méthodes de l'objet qui modifient l'état sont appelées, et il est rétabli à sa valeur d'origine lorsque l'objet sort du champ d'application. Prenons l'exemple simple suivant, qui gère les transactions, l'espace de noms actuel et l'état de $System.Process.SetZEOF:

/// Lorsqu'une instance de cette classe dépasse son champ d'application, l'espace de noms, le niveau de transaction et la valeur de $System.Process.SetZEOF() qui étaient présents lors de sa création sont restaurés.
Class DC.Demo.ScopeManager Extends %RegisteredObject
{
 
Property InitialNamespace As %String [ InitialExpression = {$Namespace} ];
 
Property InitialTransactionLevel As %String [ InitialExpression = {$TLevel} ];
 
 
Property ZEOFSetting As %Boolean [ InitialExpression = {$System.Process.SetZEOF()} ];
 
 
Method SetZEOF(pValue As %Boolean)
{
    Set ..ZEOFSetting = $System.Process.SetZEOF(.pValue)
}
 
Method %OnClose() As %Status [ Private, ServerOnly = 1 ]
{
    Set tSC = $$$OK
     
    Try {
        Set $Namespace = ..InitialNamespace
    } Catch e {
        Set tSC = $$$ADDSC(tSC,e.AsStatus())
    }
     
    Try {
        Do $System.Process.SetZEOF(..ZEOFSetting)
    } Catch e {
        Set tSC = $$$ADDSC(tSC,e.AsStatus())
    }
     
    Try {
        While $TLevel > ..InitialTransactionLevel {
            TROLLBACK 1
        }
    } Catch e {
        Set tSC = $$$ADDSC(tSC,e.AsStatus())
    }
     
    Quit tSC
}
 
}

La classe suivante illustre la manière dont la classe enregistrée ci-dessus peut être utilisée pour simplifier le nettoyage à la fin de la méthode

Class DC.Demo.Driver
{
 
ClassMethod Run()
{
    For tArgument = "good","bad" {
        Do ..LogState(tArgument,"before")
        Do ..DemoRobustMethod(tArgument)
        Do ..LogState(tArgument,"after")
    }
}
 
ClassMethod LogState(pArgument As %String, pWhen As %String)
{
    Write !,pWhen," calling DemoRobustMethod("_$$$QUOTE(pArgument)_"):"
    Write !,$c(9),"$Namespace=",$Namespace
    Write !,$c(9),"$TLevel=",$TLevel
    Write !,$c(9),"$System.Process.SetZEOF()=",$System.Process.SetZEOF()
}
 
ClassMethod DemoRobustMethod(pArgument As %String)
{
    Set tScopeManager = ##class(DC.Demo.ScopeManager).%New()
     
    Set $Namespace = "%SYS"
    TSTART
    Do tScopeManager.SetZEOF(1)
    If (pArgument = "bad") {
        // Normalement, cela poserait un gros problème. Mais grâce à tScopeManager, ce n'est pas le cas.
        Quit
    }
    TCOMMIT
}

}