Contrôler l’état global dans des UDFs scalaires Scala

Lors de la conception d’une UDF et d’un gestionnaire qui nécessite l’accès à un état partagé, vous devrez tenir compte de la façon dont Snowflake exécute des UDFs pour traiter les lignes.

La plupart des gestionnaires devraient suivre ces lignes directrices :

  • Si vous devez initialiser un état partagé qui ne change pas d’une ligne à l’autre, initialisez-le en dehors de la fonction du gestionnaire, comme dans un constructeur.

  • Rédigez votre méthode de handler de façon à ce qu’elle soit sécurisée.

  • Évitez de stocker et de partager l’état dynamique entre les lignes.

Si votre UDF ne peut pas suivre ces directives, ou si vous souhaitez mieux comprendre les raisons de ces directives, veuillez lire les sous-sections suivantes.

Partage des états entre les appels

Snowflake s’attend à ce que les UDFs scalaires soient traitées indépendamment. Le fait de s’appuyer sur un état partagé entre les appels peut entraîner un comportement inattendu. En effet, le système peut traiter les lignes dans n’importe quel ordre et répartir ces appels sur plusieurs JVMs (pour les gestionnaires écrits en Java ou Scala).

Les UDFs doivent éviter de s’appuyer sur un état partagé entre les appels vers la méthode du handler. Cependant, il existe deux situations dans lesquelles vous pourriez vouloir qu’une UDF stocke un état partagé :

  • Du code qui contient une logique d’initialisation coûteuse que vous ne voulez pas répéter pour chaque ligne.

  • Du code qui exploite l’état partagé entre les lignes, comme un cache.

Si vous devez partager un état sur plusieurs lignes, et si cet état ne change pas dans le temps, utilisez un constructeur pour créer un état partagé en définissant des variables au niveau de l’instance. Le constructeur n’est exécuté qu’une seule fois par instance, alors que le handler est appelé une fois par ligne. L’initialisation dans le constructeur est donc moins coûteuse lorsqu’un handler traite plusieurs lignes. Et comme le constructeur n’est appelé qu’une seule fois, il n’est pas nécessaire de l’écrire pour qu’il soit à l’abri des fils.

Si votre UDF stocke un état partagé qui change, votre code doit être prêt à gérer les accès concurrents à cet état.

Pour plus d’informations sur le parallélisme et l’état partagé, voir Comprendre la parallélisation et Stockage des informations d’état JVM dans cette rubrique.

Comprendre la parallélisation

Pour améliorer les performances, Snowflake effectue la parallélisation à la fois sur et dans des JVMs.

Parallélisme à travers des JVMs

Snowflake effectue la parallélisation entre les processus Worker dans un entrepôt. Chaque Worker exécute un (ou plusieurs) JVMs. Cela signifie qu’il n’y a pas d’état partagé global. Au maximum, l’état ne peut être partagé qu’au sein d’un seul JVM.

Parallélisme au sein des JVMs

  • Chaque JVM peut exécuter plusieurs threads qui peuvent appeler la méthode du handler de la même instance en parallèle. Cela signifie que chaque méthode du handler doit être à l’abri des fils.

  • Si une IMMUTABLE est SQL et qu’une instruction UDF appelle la même UDF plusieurs fois avec les mêmes arguments pour la même ligne, alors l’UDF renvoie la même valeur pour chaque appel pour cette ligne.

    Par exemple, la commande suivante renvoie deux fois la même valeur si l’UDF est IMMUTABLE :

    SELECT my_scala_udf(42), my_scala_udf(42) FROM table1;
    
    Copy

    Si vous souhaitez que plusieurs appels renvoient des valeurs indépendantes même s’ils reçoivent les mêmes arguments, et si vous ne souhaitez pas déclarer la fonction VOLATILE, liez plusieurs UDFs distinctes à la même méthode de traitement.

    Pour ce faire, vous pouvez suivre ces étapes.

    1. Créez un fichier JAR nommé @udf_libs/rand.jar avec le code suivant :

      class MyClass {
      
          var x: Double = 0.0
      
          // Constructor
          def this() = {
              x = Math.random()
          }
      
          // Handler
          def myHandler(): Double = x
      }
      
      Copy
    2. Créez des UDFs Scala comme indiqué ci-dessous.

      Ces UDFs portent des noms différents, mais utilisent le même fichier JAR et le même handler dans ce fichier JAR.

      CREATE FUNCTION my_scala_udf_1()
        RETURNS DOUBLE
        LANGUAGE SCALA
        IMPORTS = ('@udf_libs/rand.jar')
        HANDLER = 'MyClass.myHandler';
      
      CREATE FUNCTION my_scala_udf_2()
        RETURNS DOUBLE
        LANGUAGE SCALA
        IMPORTS = ('@udf_libs/rand.jar')
        HANDLER = 'MyClass.myHandler';
      
      Copy
    3. Utilisez le code suivant pour appeler les deux UDFs.

      Les UDFs pointent vers le même fichier JAR et le même handler. Ces appels créent deux instances de la même classe. Chaque instance renvoie une valeur indépendante, de sorte que l’exemple ci-dessous renvoie deux valeurs indépendantes, plutôt que de renvoyer deux fois la même valeur :

      SELECT my_scala_udf_1(), my_scala_udf_2() FROM table1;
      
      Copy

Stockage des informations d’état JVM

Une raison d’éviter de s’appuyer sur un état partagé dynamique est que les lignes ne sont pas nécessairement traitées dans un ordre prévisible. Chaque fois qu’une instruction SQL est exécutée, Snowflake peut faire varier le nombre de lots, l’ordre dans lequel les lots sont traités et l’ordre des lignes dans un lot. Si une UDF scalaire est conçue de telle sorte qu’une ligne affecte la valeur de retour d’une ligne suivante, alors l” UDF peut renvoyer des résultats différents chaque fois que l” UDF est exécutée.