The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Propagação de Contexto no Quarkus

Traditional blocking code uses ThreadLocal variables to store contextual objects in order to avoid passing them as parameters everywhere. Many Quarkus extensions require those contextual objects to operate properly: Quarkus REST (formerly RESTEasy Reactive), ArC and Transaction for example.

Se você escrever código reativo/assíncrono, terá de dividir seu trabalho em um pipeline de blocos de código que serão executados "mais tarde" e, na prática, após o retorno do método em que você os definiu. Dessa forma, os blocos try/finally e as variáveis ThreadLocal param de funcionar, porque o código reativo é executado em outro thread, depois que o chamador executou o bloco finally.

A Propagação de Contexto do SmallRye, uma implementação da Propagação de Contexto do MicroProfile, foi criada para fazer com que essas extensões do Quarkus funcionem corretamente em configurações reativas/assíncronas. Ela funciona capturando os valores contextuais que costumavam estar em contextos locais de thread e os restaurando quando o código é chamado.

Solução

Recomendamos que siga as instruções nas seções seguintes e crie a aplicação passo a passo. No entanto, você pode ir diretamente para o exemplo completo.

Clone o repositório Git: git clone https://github.com/quarkusio/quarkus-quickstarts.git, ou baixe um arquivo.

A solução está localizada no diretório context-propagation-quickstart.

Configuração

Se você estiver usando o Mutiny (a extensão quarkus-mutiny), basta adicionar a extensão quarkus-smallrye-context-propagation para ativar a propagação de contexto.

Em outras palavras, adicione as seguintes dependências ao seu arquivo de construção:

pom.xml
<!-- Quarkus REST extension if not already included -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest</artifactId>
</dependency>
<!-- Context Propagation extension -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-context-propagation</artifactId>
</dependency>
build.gradle
// Quarkus REST extension if not already included
implementation("io.quarkus:quarkus-rest")
// Context Propagation extension
implementation("io.quarkus:quarkus-smallrye-context-propagation")

With this, you will get context propagation for ArC, Quarkus REST and transactions, if you are using them.

Exemplo de uso com Mutiny

Mutiny

Esta seção usa os tipos reativos do Mutiny. Se você não estiver familiarizado com o Mutiny, consulte Mutiny - uma biblioteca de programação reativa intuitiva.

Vamos escrever um endpoint REST que leia os próximos 3 itens de um tópico do Kafka, armazene-os em um banco de dados usando o Hibernate ORM com Panache (tudo na mesma transação) antes de devolvê-los ao cliente:

    // Get the prices stream
    @Inject
    @Channel("prices") Publisher<Double> prices;

    @Transactional
    @GET
    @Path("/prices")
    @RestStreamElementType(MediaType.TEXT_PLAIN)
    public Publisher<Double> prices() {
        // get the next three prices from the price stream
        return Multi.createFrom().publisher(prices)
                .select().first(3)
                // The items are received from the event loop, so cannot use Hibernate ORM (classic)
                // Switch to a worker thread, the transaction will be propagated
                .emitOn(Infrastructure.getDefaultExecutor())
                .map(price -> {
                    // store each price before we send them
                    Price priceEntity = new Price();
                    priceEntity.value = price;
                    // here we are all in the same transaction
                    // thanks to context propagation
                    priceEntity.persist();
                    return price;
                    // the transaction is committed once the stream completes
                });
    }

Observe que, graças ao suporte do Mutiny para propagação de contexto, isso funciona imediatamente. Os três itens são mantidos usando a mesma transação e essa transação é confirmada quando o fluxo é concluído.

Exemplo de uso para CompletionStage

Se você estiver usando CompletionStage você precisa de propagação manual de contexto. Você pode fazer isso injetando um ThreadContext ou ManagedExecutor que propagará cada contexto. Por exemplo, aqui usamos o Cliente Web do Vert.x para obter a lista de pessoas do Star Wars e, em seguida, armazená-las no banco de dados usando o Hibernate ORM com Panache (tudo na mesma transação) antes de retorná-las ao cliente como JSON usando Jackson ou JSON-B :

    @Inject ThreadContext threadContext;
    @Inject ManagedExecutor managedExecutor;
    @Inject Vertx vertx;

    @Transactional
    @GET
    @Path("/people")
    public CompletionStage<List<Person>> people() throws SystemException {
        // Create a REST client to the Star Wars API
        WebClient client = WebClient.create(vertx,
                         new WebClientOptions()
                          .setDefaultHost("swapi.dev")
                          .setDefaultPort(443)
                          .setSsl(true));
        // get the list of Star Wars people, with context capture
        return threadContext.withContextCapture(client.get("/api/people/").send())
                .thenApplyAsync(response -> {
                    JsonObject json = response.bodyAsJsonObject();
                    List<Person> persons = new ArrayList<>(json.getInteger("count"));
                    // Store them in the DB
                    // Note that we're still in the same transaction as the outer method
                    for (Object element : json.getJsonArray("results")) {
                        Person person = new Person();
                        person.name = ((JsonObject) element).getString("name");
                        person.persist();
                        persons.add(person);
                    }
                    return persons;
                }, managedExecutor);
    }

Usando ThreadContext ou ManagedExecutor, você pode envolver os tipos funcionais mais úteis e CompletionStage para que o contexto seja propagado.

O site ManagedExecutor injetado usa o pool de threads do Quarkus.

Sobrescrevendo quais contextos são propagados

Por padrão, todos os contextos disponíveis são propagados. No entanto, você pode sobrescrever esse comportamento de várias maneiras.

Usando a configuração

As propriedades de configuração a seguir permitem que você especifique os conjuntos padrão de contextos propagados:

Propriedade de configuração Descrição Valor padrão

mp.context.ThreadContext.propagated

O conjunto de contextos propagados, separado por vírgulas

Remaining (todos os contextos de lista não explícita)

mp.context.ThreadContext.cleared

O conjunto de contextos liberados separados por vírgulas

None (sem contexto), a menos que nem os conjuntos propagados nem os limpos contenham Remaining, caso em que o padrão é Remaining (todos os contextos não listados explicitamente)

mp.context.ThreadContext.unchanged

O conjunto de contextos inalterados, separado por vírgulas

None (sem contexto)

Os contextos a seguir estão disponíveis no Quarkus imediatamente ou dependendo da inclusão de suas extensões:

Nome do Contexto Nome Constante Descrição

None

ThreadContext.NONE

Pode ser usado para especificar um conjunto vazio de contextos, mas definir o valor como vazio também funciona

Remaining

ThreadContext.ALL_REMAINING

Todos os contextos que não estão explicitamente listados em outros conjuntos

Transaction

ThreadContext.TRANSACTION

O contexto da transação JTA

CDI

ThreadContext.CDI

O contexto do CDI (ArC)

Servlet

N/A

O contexto do servlet

Jakarta REST

N/A

The Quarkus REST or RESTEasy Classic context

Application

ThreadContext.APPLICATION

O ThreadContextClassLoader atual

Sobrescrevendo os contextos propagados usando anotações

Para que a propagação automática de contexto, como o Mutiny usa, seja sobrescrita em métodos específicos, você pode usar a anotação @CurrentThreadContext:

    // Get the prices stream
    @Inject
    @Channel("prices") Publisher<Double> prices;

    @GET
    @Path("/prices")
    @RestStreamElementType(MediaType.TEXT_PLAIN)
    // Get rid of all context propagation, since we don't need it here
    @CurrentThreadContext(propagated = {}, unchanged = ThreadContext.ALL_REMAINING)
    public Publisher<Double> prices() {
        // get the next three prices from the price stream
        return Multi.createFrom().publisher(prices)
                .select().first(3);
    }

Sobrescrevendo os contextos propagados usando injeção de CDI

Você também pode injetar um ThreadContext personalizado usando a anotação @ThreadContextConfig em seu ponto de injeção:

    // Get the prices stream
    @Inject
    @Channel("prices") Publisher<Double> prices;
    // Get a ThreadContext that doesn't propagate context
    @Inject
    @ThreadContextConfig(unchanged = ThreadContext.ALL_REMAINING)
    SmallRyeThreadContext threadContext;

    @GET
    @Path("/prices")
    @RestStreamElementType(MediaType.TEXT_PLAIN)
    public Publisher<Double> prices() {
        // Get rid of all context propagation, since we don't need it here
        try(CleanAutoCloseable ac = SmallRyeThreadContext.withThreadContext(threadContext)){
            // get the next three prices from the price stream
            return Multi.createFrom().publisher(prices)
                    .select().first(3);
        }
    }

Da mesma forma, há uma maneira semelhante de injetar uma instância configurada de ManagedExecutor usando a anotação @ManagedExecutorConfig:

    // Custom ManagedExecutor with different async limit, queue and no propagation
    @Inject
    @ManagedExecutorConfig(maxAsync = 2, maxQueued = 3, cleared = ThreadContext.ALL_REMAINING)
    ManagedExecutor configuredCustomExecutor;

Compartilhando instâncias de CDI configuradas de ManagedExecutor e ThreadContext

Se você precisar injetar o mesmo ManagedExecutor ou ThreadContext em vários locais e compartilhar sua capacidade, você pode nomear a instância com a anotação @NamedInstance. @NamedInstance é um qualificador de CDI e, portanto, todas as injeções do mesmo tipo e nome compartilharão a mesma instância subjacente. Se também precisar personalizar a instância, você pode fazer isso usando a anotação @ManagedExecutorConfig / ThreadContextConfig em um dos pontos de injeção:

    // Custom configured ManagedExecutor with name
    @Inject
    @ManagedExecutorConfig(maxAsync = 2, maxQueued = 3, cleared = ThreadContext.ALL_REMAINING)
    @NamedInstance("myExecutor")
    ManagedExecutor sharedConfiguredExecutor;

    // Since this executor has the same name, it will be the same instance as above
    @Inject
    @NamedInstance("myExecutor")
    ManagedExecutor sameExecutor;

    // Custom ThreadContext with a name
    @Inject
    @ThreadContextConfig(unchanged = ThreadContext.ALL_REMAINING)
    @NamedInstance("myContext")
    ThreadContext sharedConfiguredThreadContext;

    // Given equal value of @NamedInstance, this ThreadContext will be the same as the above one
    @Inject
    @NamedInstance("myContext")
    ThreadContext sameContext;

Propagação de Contexto para CDI

Em termos de CDI, os beans @RequestScoped, @ApplicationScoped e @Singleton são propagados e ficam disponíveis em outros threads. Os beans @Dependent, bem como quaisquer beans com escopo personalizado, não podem ser propagados automaticamente por meio da Propagação de Contexto do CDI.

Beans @ApplicationScoped e @Singleton são sempre escopos ativos e, portanto, são fáceis de lidar - as tarefas de propagação de contexto podem funcionar com esses beans enquanto o contêiner CDI estiver em execução. Entretanto, os beans @RequestScoped são uma história diferente. Eles só ficam ativos por um curto período de tempo, que pode ser vinculado a uma solicitação HTTP ou a alguma outra solicitação/tarefa quando ativados/desativados manualmente. Nesse caso, o usuário deve estar ciente de que, quando o thread original chegar ao fim de uma solicitação, ele encerrará o contexto, chamando @PreDestroy nesses beans e, em seguida, limpando-os do contexto. As tentativas subsequentes de acessar esses beans de outros threads podem resultar em um comportamento inesperado. Portanto, é recomendável garantir que todas as tarefas que usam beans com escopo de solicitação por meio da propagação de contexto sejam executadas de forma que não ultrapassem a duração da solicitação original.

Devido ao comportamento descrito acima, recomenda-se evitar o uso de @PreDestroy em @RequestScoped beans ao trabalhar com a propagação de contexto no CDI.