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.