Depurando fluxos reativos na primavera 5
1. Visão geral
Depurarreactive streams é provavelmente um dos principais desafios que teremos que enfrentar assim que começarmos a usar essas estruturas de dados.
E tendo em mente que os fluxos reativos têm ganhado popularidade nos últimos anos, é uma boa ideia saber como podemos realizar essa tarefa de forma eficiente.
Vamos começar configurando um projeto usando uma pilha reativa para ver por que isso costuma ser problemático.
2. Cenário com erros
Queremos simular um cenário de caso real, onde vários processos assíncronos estão em execução e onde introduzimos alguns defeitos no código que acabarão por acionar exceções.
Para entender o quadro geral, mencionaremos que nosso aplicativo consumirá e processará fluxos de objetosFoo simples que contêm apenas umid, umformattedName e umquantitycampo s. Para obter mais detalhes, consulteon the project here.
2.1. Analisando a saída do log
Agora, vamos examinar um snippet e a saída que ele gera quando um erro não tratado aparece:
public void processFoo(Flux flux) {
flux = FooNameHelper.concatFooName(flux);
flux = FooNameHelper.substringFooName(flux);
flux = FooReporter.reportResult(flux);
flux.subscribe();
}
public void processFooInAnotherScenario(Flux flux) {
flux = FooNameHelper.substringFooName(flux);
flux = FooQuantityHelper.divideFooQuantity(flux);
flux.subscribe();
}
Depois de executar nosso aplicativo por alguns segundos, perceberemos que ele está registrando exceções de vez em quando.
Olhando de perto um dos erros, encontraremos algo semelhante a isto:
Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 15
at j.l.String.substring(String.java:1963)
at com.example.debugging.consumer.service.FooNameHelper
.lambda$1(FooNameHelper.java:38)
at r.c.p.FluxMap$MapSubscriber.onNext(FluxMap.java:100)
at r.c.p.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at r.c.p.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:275)
at r.c.p.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:849)
at r.c.p.Operators$MonoSubscriber.complete(Operators.java:1476)
at r.c.p.MonoDelayUntil$DelayUntilCoordinator.signal(MonoDelayUntil.java:211)
at r.c.p.MonoDelayUntil$DelayUntilTrigger.onComplete(MonoDelayUntil.java:290)
at r.c.p.MonoDelay$MonoDelayRunnable.run(MonoDelay.java:118)
at r.c.s.SchedulerTask.call(SchedulerTask.java:50)
at r.c.s.SchedulerTask.call(SchedulerTask.java:27)
at j.u.c.FutureTask.run(FutureTask.java:266)
at j.u.c.ScheduledThreadPoolExecutor$ScheduledFutureTask
.access$201(ScheduledThreadPoolExecutor.java:180)
at j.u.c.ScheduledThreadPoolExecutor$ScheduledFutureTask
.run(ScheduledThreadPoolExecutor.java:293)
at j.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at j.u.c.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at j.l.Thread.run(Thread.java:748)
Com base na causa raiz, e observando a classeFooNameHelper mencionada no rastreamento da pilha, podemos imaginar que em algumas ocasiões, nossos objetosFoo estão sendo processados com um valorformattedName menor que esperado.
Obviamente, este é apenas um caso simplificado, e a solução parece bastante óbvia.
Mas vamos imaginar que este seja um cenário real em que a exceção em si não nos ajude a resolver o problema sem algumas informações de contexto.
A exceção foi disparada como parte do métodoprocessFoo, ouprocessFooInAnotherScenario?
As outras etapas anteriores afetaram o campoformattedName antes de chegar a este estágio?
A entrada de registro não nos ajudaria a resolver essas questões.
Para piorar as coisas, às vezes a exceção nem mesmo é lançada de dentro de nossa funcionalidade.
Por exemplo, imagine que dependemos de um repositório reativo para persistir nossos objetosFoo. Se ocorrer um erro nesse ponto, talvez nem tenhamos uma pista de onde começar a depurar nosso código.
Precisamos de ferramentas para depurar fluxos reativos com eficiência.
3. Usando uma sessão de depuração
Uma opção para descobrir o que está acontecendo com nosso aplicativo é iniciar uma sessão de depuração usando nosso IDE favorito.
Teremos que configurar alguns pontos de interrupção condicionais e analisar o fluxo de dados quando cada etapa do fluxo for executada.
Na verdade, esta pode ser uma tarefa complicada, especialmente quando temos muitos processos reativos em execução e compartilhando recursos.
Além disso, existem muitas circunstâncias em que não podemos iniciar uma sessão de depuração por motivos de segurança.
4. Registro de informações com o métododoOnError ou usando o parâmetro de assinatura
Às vezes,we can add useful context information, by providing a Consumer as a second parameter of the subscribe method:
public void processFoo(Flux flux) {
// ...
flux.subscribe(foo -> {
logger.debug("Finished processing Foo with Id {}", foo.getId());
}, error -> {
logger.error(
"The following error happened on processFoo method!",
error);
});
}
Observação: vale a pena mencionar que, se não precisarmos realizar mais processamento no métodosubscribe, podemos encadear a funçãodoOnError em nosso editor:
flux.doOnError(error -> {
logger.error("The following error happened on processFoo method!", error);
}).subscribe();
Agora teremos algumas orientações sobre a origem do erro, embora ainda não tenhamos muitas informações sobre o elemento real que gerou a exceção.
5. Ativando a configuração de depuração global do Reactor
A bibliotecaReactor fornece uma classeHooks que nos permite configurar o comportamento dos operadoresFluxeMono.
By just adding the following statement, our application will instrument the calls to the to the publishers' methods, wrap the construction of the operator, and capture a stack trace:
Hooks.onOperatorDebug();
Após a ativação do modo de depuração, nossos logs de exceção incluirão algumas informações úteis:
16:06:35.334 [parallel-1] ERROR c.b.d.consumer.service.FooService
- The following error happened on processFoo method!
java.lang.StringIndexOutOfBoundsException: String index out of range: 15
at j.l.String.substring(String.java:1963)
at c.d.b.c.s.FooNameHelper.lambda$1(FooNameHelper.java:38)
...
at j.l.Thread.run(Thread.java:748)
Suppressed: r.c.p.FluxOnAssembly$OnAssemblyException:
Assembly trace from producer [reactor.core.publisher.FluxMapFuseable] :
reactor.core.publisher.Flux.map(Flux.java:5653)
c.d.b.c.s.FooNameHelper.substringFooName(FooNameHelper.java:32)
c.d.b.c.s.FooService.processFoo(FooService.java:24)
c.d.b.c.c.ChronJobs.consumeInfiniteFlux(ChronJobs.java:46)
o.s.s.s.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
o.s.s.s.DelegatingErrorHandlingRunnable
.run(DelegatingErrorHandlingRunnable.java:54)
o.u.c.Executors$RunnableAdapter.call(Executors.java:511)
o.u.c.FutureTask.runAndReset(FutureTask.java:308)
Error has been observed by the following operator(s):
|_ Flux.map ⇢ c.d.b.c.s.FooNameHelper
.substringFooName(FooNameHelper.java:32)
|_ Flux.map ⇢ c.d.b.c.s.FooReporter.reportResult(FooReporter.java:15)
Como podemos ver, a primeira seção permanece relativamente a mesma, mas as seções a seguir fornecem informações sobre:
-
O rastreamento de montagem do editor - aqui podemos confirmar que o erro foi gerado pela primeira vez no métodoprocessFoo.
-
Os operadores que observaram o erro depois que ele foi disparado pela primeira vez, com a classe de usuário em que foram encadeados.
Nota: Neste exemplo, principalmente para ver isso claramente, estamos adicionando as operações em diferentes classes.
Podemos ativar ou desativar o modo de depuração a qualquer momento, mas isso não afetará os objetosFluxeMono que já foram instanciados.
5.1. Executando operadores em diferentes segmentos
Um outro aspecto a ter em mente é que o rastreamento de montagem é gerado corretamente, mesmo se houver diferentes threads operando no fluxo.
Vejamos o seguinte exemplo:
public void processFoo(Flux flux) {
flux = flux.publishOn(Schedulers.newSingle("foo-thread"));
// ...
flux = flux.publishOn(Schedulers.newSingle("bar-thread"));
flux = FooReporter.reportResult(flux);
flux.subscribeOn(Schedulers.newSingle("starter-thread"))
.subscribe();
}
Agora, se verificarmos os registros, apreciaremos que, neste caso, a primeira seção pode mudar um pouco, mas as duas últimas permanecem praticamente as mesmas.
A primeira parte é o rastreamento de pilha de thread, portanto, ele mostrará apenas as operações realizadas por um determinado thread.
Como vimos, essa não é a seção mais importante quando estamos depurando o aplicativo, então essa mudança é aceitável.
6. Ativando a saída de depuração em um único processo
Instrumentar e gerar um rastreamento de pilha em cada processo reativo é caro.
Portanto,we should implement the former approach only in critical cases.
De qualquer forma,Reactor provides a way to enable the debug mode on single crucial processes, which is less memory-consuming.
Estamos nos referindo ao operadorcheckpoint:
public void processFoo(Flux flux) {
// ...
flux = flux.checkpoint("Observed error on processFoo", true);
flux.subscribe();
}
Observe que dessa maneira, o rastreamento de montagem será registrado no estágio do ponto de verificação:
Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 15
...
Assembly trace from producer [reactor.core.publisher.FluxMap],
described as [Observed error on processFoo] :
r.c.p.Flux.checkpoint(Flux.java:3096)
c.b.d.c.s.FooService.processFoo(FooService.java:26)
c.b.d.c.c.ChronJobs.consumeInfiniteFlux(ChronJobs.java:46)
o.s.s.s.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
o.s.s.s.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
j.u.c.Executors$RunnableAdapter.call(Executors.java:511)
j.u.c.FutureTask.runAndReset(FutureTask.java:308)
Error has been observed by the following operator(s):
|_ Flux.checkpoint ⇢ c.b.d.c.s.FooService.processFoo(FooService.java:26)
Devemos implementar o métodocheckpoint no final da cadeia reativa.
Caso contrário, o operador não será capaz de observar erros ocorrendo a jusante.
Além disso, observe que a biblioteca oferece um método sobrecarregado. Podemos evitar:
-
especificando uma descrição para o erro observado se usarmos a opção no-args
-
gerar um rastreamento de pilha preenchido (que é a operação mais cara), fornecendo apenas a descrição personalizada
7. Registrando uma sequência de elementos
Por fim, os editores do Reactor oferecem mais um método que pode ser útil em alguns casos.
By calling the log method in our reactive chain, the application will log each element in the flow with the state that it has at that stage.
Vamos experimentar em nosso exemplo:
public void processFoo(Flux flux) {
flux = FooNameHelper.concatFooName(flux);
flux = FooNameHelper.substringFooName(flux);
flux = flux.log();
flux = FooReporter.reportResult(flux);
flux = flux.doOnError(error -> {
logger.error("The following error happened on processFoo method!", error);
});
flux.subscribe();
}
E verifique os logs:
INFO reactor.Flux.Map.1 - onSubscribe(FluxMap.MapSubscriber)
INFO reactor.Flux.Map.1 - request(unbounded)
INFO reactor.Flux.Map.1 - onNext(Foo(id=0, formattedName=theFo, quantity=8))
INFO reactor.Flux.Map.1 - onNext(Foo(id=1, formattedName=theFo, quantity=3))
INFO reactor.Flux.Map.1 - onNext(Foo(id=2, formattedName=theFo, quantity=5))
INFO reactor.Flux.Map.1 - onNext(Foo(id=3, formattedName=theFo, quantity=6))
INFO reactor.Flux.Map.1 - onNext(Foo(id=4, formattedName=theFo, quantity=6))
INFO reactor.Flux.Map.1 - cancel()
ERROR c.b.d.consumer.service.FooService
- The following error happened on processFoo method!
...
Podemos ver facilmente o estado de cada objetoFoo neste estágio e como o framework cancela o fluxo quando ocorre uma exceção.
Claro, essa abordagem também é cara e teremos que usá-la com moderação.
8. Conclusão
Podemos consumir muito do nosso tempo e esforço resolvendo problemas se não conhecermos as ferramentas e mecanismos para depurar nosso aplicativo adequadamente.
Isso é especialmente verdadeiro se não estamos acostumados a lidar com estruturas de dados reativas e assíncronas e precisamos de ajuda extra para descobrir como as coisas funcionam.
Como sempre, o exemplo completo está disponível emGitHub repo.