Controle do estado global em UDFs escalares em Scala

Ao projetar uma UDF e um manipulador que exija acesso ao estado compartilhado, você precisará levar em conta a maneira como o Snowflake executa as UDFs para processar as linhas.

A maioria dos manipuladores deve seguir essas diretrizes:

  • Se você precisar inicializar o estado compartilhado que não muda entre linhas, inicialize-o fora da função do manipulador, como no construtor.

  • Escreva seu método do manipulador de forma a ser thread-safe.

  • Evite armazenar e compartilhar o estado dinâmico em linhas diferentes.

Se sua UDF não puder seguir essas diretrizes, ou se você quiser entender mais profundamente as razões para essas diretrizes, leia as próximas subseções.

Compartilhamento de estado por chamadas

O Snowflake espera que o UDFs escalares seja processadas de forma independente. Confiar no estado compartilhado entre invocações pode resultar em um comportamento inesperado. Isso acontece pois o sistema pode processar linhas em qualquer ordem e distribuir essas invocações por vários JVMs (para manipuladores escritos em Java ou Scala).

UDFs devem evitar confiar em um estado compartilhado entre diferentes chamadas para o método do manipulador. Entretanto, há duas situações em que você pode querer que uma UDF armazene um estado compartilhado:

  • Código que contém uma lógica de inicialização cara que você não quer repetir para cada linha.

  • Código que use um estado compartilhado em linhas diferentes, como um cache.

Se você precisar compartilhar o estado em várias linhas, e se esse estado não mudar com o tempo, use um construtor para criar um estado compartilhado definindo variáveis em nível de instância. O construtor é executado apenas uma vez por instância, enquanto o manipulador é chamado uma vez por linha. Portanto, inicializar no construtor é mais barato quando um manipulador processa várias linhas. E como o construtor é chamado apenas uma vez, o construtor não precisa ser escrito de forma a ser thread-safe.

Se sua UDF armazena um estado compartilhado que muda, seu código deve estar preparado para lidar com o acesso simultâneo a esse estado.

Para obter mais informações sobre paralelismo e estado compartilhado, consulte O que é paralelização e Como armazenar informações de estado de uma JVM neste tópico.

O que é paralelização

Para melhorar o desempenho, o Snowflake paraleliza tanto entre JVMs diferente quanto dentro de JVMs.

Paralelização entre JVMs

O Snowflake paralela entre os trabalhadores em um warehouse. Cada trabalhador executa uma (ou mais) JVMs. Isso significa que não existe um estado global compartilhado. No máximo, o estado só pode ser compartilhado dentro de uma única JVM.

Paralelização dentro de JVMs

  • Cada JVM pode executar vários threads que podem chamar o mesmo método do manipulador da mesma instância em paralelo. Isso significa que cada método do manipulador precisa ser thread-safe.

  • Se uma UDF é IMMUTABLE e uma instrução SQL chama a UDF mais de uma vez com os mesmos argumentos para a mesma linha, a UDF retorna o mesmo valor para cada chamada para aquela linha.

    Por exemplo, o código a seguir retorna o mesmo valor duas vezes para cada linha se a UDF for IMMUTABLE:

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

    Se você quiser que múltiplas chamadas retornem valores independentes mesmo quando passarem os mesmos argumentos não quiser declarar a função como VOLATILE, ligue diferentes chamadas a UDFs separadas ao mesmo método do manipulador.

    Você pode fazer isso usando as etapas a seguir.

    1. Crie um arquivo JAR denominado @udf_libs/rand.jar com o seguinte código:

      class MyClass {
      
          var x: Double = 0.0
      
          // Constructor
          def this() = {
              x = Math.random()
          }
      
          // Handler
          def myHandler(): Double = x
      }
      
      Copy
    2. Crie as UDFs de Scala conforme mostrado abaixo.

      Essas UDFs têm nomes diferentes, mas usam o mesmo arquivo JAR e o mesmo manipulador dentro desse arquivo 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. Use o código a seguir para chamar ambas as UDFs.

      As UDFs apontam para o mesmo arquivo e manipulador JAR. Essas chamadas criam duas instâncias da mesma classe. Cada instância retorna um valor independente, portanto o exemplo abaixo retorna dois valores independentes em vez de retornar o mesmo valor duas vezes:

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

Como armazenar informações de estado de uma JVM

Uma razão para evitar confiar no estado dinâmico compartilhado é que as linhas não são necessariamente processadas em uma ordem previsível. Cada vez que uma instrução SQL é executada, o Snowflake pode variar o número de lotes, a ordem em que os lotes são processados e a ordem das linhas dentro de um lote. Se uma UDF escalar for projetada para que uma linha afete o valor de retorno para uma linha posterior, a UDF pode retornar resultados diferentes cada vez que a UDF for executada.