Lidando com a contrapressão com o RxJava
1. Visão geral
Neste artigo, veremos comoRxJava library nos ajuda a lidar com a contrapressão.
Simplificando - RxJava utiliza um conceito de fluxos reativos, introduzindoObservables, para o qual um ou muitosObservers podem se inscrever. Dealing with possibly infinite streams is very challenging, as we need to face a problem of a backpressure.
Não é difícil entrar em uma situação em que umObservable está emitindo itens mais rapidamente do que um assinante pode consumi-los. Examinaremos as diferentes soluções para o problema do crescente buffer de itens não consumidos.
2. QuenteObservables Versus FrioObservables
Primeiro, vamos criar uma função de consumidor simples que será usada como consumidor de elementos deObservables que definiremos mais tarde:
public class ComputeFunction {
public static void compute(Integer v) {
try {
System.out.println("compute integer v: " + v);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Nossa funçãocompute() simplesmente imprime o argumento. O importante a notar aqui é a invocação de um métodoThread.sleep(1000) - estamos fazendo isso para emular alguma tarefa de longa execução que fará com queObservable se encha com itens mais rápidos queObserver pode consumir eles.
Temos dois tipos deObservables – Hot eCold - que são totalmente diferentes no que diz respeito ao tratamento da contrapressão.
2.1. FrioObservables
UmObservable frio emite uma sequência particular de itens, mas pode começar a emitir esta sequência quando seuObserver achar conveniente, e a qualquer taxa que oObserver desejar, sem interromper a integridade do seqüência. Cold Observable is providing items in a lazy way.
OObserver está pegando elementos apenas quando está pronto para processar aquele item, e os itens não precisam ser armazenados em buffer em umObservable porque são solicitados de forma pull.
Por exemplo, se você criar umObservable com base em um intervalo estático de elementos de um a um milhão, esseObservable emitirá a mesma sequência de itens, independentemente da frequência com que esses itens sejam observados:
Observable.range(1, 1_000_000)
.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute);
Quando iniciarmos nosso programa, os itens serão calculados porObserver preguiçosamente e serão solicitados de forma pull. O métodoSchedulers.computation() significa que queremos executar nossoObserver dentro de um pool de threads de computação emRxJava.
A saída de um programa consistirá em um resultado de um métodocompute() invocado para um por um item de umObservable:
compute integer v: 1
compute integer v: 2
compute integer v: 3
compute integer v: 4
...
ColdObservables não precisa ter qualquer forma de contrapressão porque eles funcionam de forma puxada. Exemplos de itens emitidos por um coldObservable podem incluir os resultados de uma consulta de banco de dados, recuperação de arquivo ou solicitação da web.
2.2. QuenteObservables
UmObservable quente começa a gerar itens e emite-os imediatamente quando são criados. É contrário a um modelo de processamento pull de ColdObservables. Hot Observable emits items at its own pace, and it is up to its observers to keep up.
QuandoObserver não é capaz de consumir itens tão rapidamente quanto são produzidos por umObservable, eles precisam ser armazenados em buffer ou manipulados de alguma outra forma, pois irão preencher a memória, causando finalmenteOutOfMemoryException.
Vamos considerar um exemplo deObservable, quente que está produzindo 1 milhão de itens para um consumidor final que está processando esses itens. Quando um métodocompute() emObserver leva algum tempo para processar todos os itens, oObservable está começando a preencher a memória com itens, fazendo com que um programa falhe:
PublishSubject source = PublishSubject.create();
source.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute, Throwable::printStackTrace);
IntStream.range(1, 1_000_000).forEach(source::onNext);
A execução desse programa irá falhar com umMissingBackpressureException porque não definimos uma maneira de lidar com a superprodução deObservable.
Exemplos de itens emitidos por umObservable quente podem incluir eventos de mouse e teclado, eventos do sistema ou preços de ações.
3. Superprodução de bufferObservable
A primeira maneira de lidar com a superprodução deObservable é definir algum tipo de buffer para elementos que não podem ser processados por umObserver.
Podemos fazer isso chamando um métodobuffer():
PublishSubject source = PublishSubject.create();
source.buffer(1024)
.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute, Throwable::printStackTrace);
Definir um buffer com um tamanho de 1024 dará aoObserver algum tempo para alcançar uma fonte de superprodução. O buffer armazenará itens que ainda não foram processados.
Podemos aumentar o tamanho do buffer para ter espaço suficiente para os valores produzidos.
No entanto, observe que, geralmente,this may be only a temporary fix como estouro ainda pode ocorrer se a fonte superproduzir o tamanho do buffer previsto.
4. Lote de itens emitidos
Podemos agrupar itens superproduzidos em janelas com N elementos.
QuandoObservable está produzindo elementos mais rápido do queObserver pode processá-los, podemos aliviar isso agrupando os elementos produzidos e enviando um lote de elementos paraObserver que é capaz de processar uma coleção de elementos em vez de um elemento por um:
PublishSubject source = PublishSubject.create();
source.window(500)
.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute, Throwable::printStackTrace);
Usar o métodowindow() com o argumento500, dirá aObservable para agrupar elementos em lotes de 500 tamanhos. Essa técnica pode reduzir o problema de superprodução deObservable quandoObserver é capaz de processar um lote de elementos mais rapidamente em comparação com o processamento de elementos um por um.
5. Ignorando Elementos
Se alguns dos valores produzidos porObservable podem ser ignorados com segurança, podemos usar a amostragem dentro de um tempo específico e operadores de limitação.
Os métodossample()ethrottleFirst() estão tomando a duração como parâmetro:
-
O método sample() periodicamente examina a sequência de elementos e emite o último item que foi produzido dentro da duração especificada como um parâmetro
-
O métodothrottleFirst() emite o primeiro item que foi produzido após a duração especificada como parâmetro
A duração é um tempo após o qual um elemento específico é selecionado da sequência de elementos produzidos. Podemos especificar uma estratégia para lidar com a contrapressão pulando elementos:
PublishSubject source = PublishSubject.create();
source.sample(100, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute, Throwable::printStackTrace);
Especificamos que a estratégia de pular elementos será um métodosample(). Queremos uma amostra de uma sequência com duração de 100 milissegundos. Esse elemento será emitido para oObserver.
Lembre-se, no entanto, que esses operadores reduzem a taxa de recepção de valor apenas emObserver a jusante e, portanto, ainda podem levar aMissingBackpressureException.
6. Manipulando um buffer de preenchimentoObservable
Caso nossas estratégias de amostragem ou lote de elementos não ajudem no preenchimento de um buffer,, precisamos implementar uma estratégia de tratamento de casos quando um buffer está enchendo.
Precisamos usar um métodoonBackpressureBuffer() para evitarBufferOverflowException.
O métodoonBackpressureBuffer() leva três argumentos: a capacidade de um bufferObservable, um método que é chamado quando um buffer está enchendo e uma estratégia para lidar com elementos que precisam ser descartados de um buffer. As estratégias para estouro estão em uma classeBackpressureOverflow.
Existem 4 tipos de ações que podem ser executadas quando o buffer é preenchido:
-
ON_OVERFLOW_ERROR – este é o comportamento padrão sinalizando umBufferOverflowException quando o buffer está cheio
-
ON_OVERFLOW_DEFAULT – atualmente é o mesmo queON_OVERFLOW_ERROR
-
ON_OVERFLOW_DROP_LATEST - se ocorrer um estouro, o valor atual será simplesmente ignorado e apenas os valores antigos serão entregues uma vez que as solicitações deObserver downstream
-
ON_OVERFLOW_DROP_OLDEST - descarta o elemento mais antigo no buffer e adiciona o valor atual a ele
Vamos ver como especificar essa estratégia:
Observable.range(1, 1_000_000)
.onBackpressureBuffer(16, () -> {}, BackpressureOverflow.ON_OVERFLOW_DROP_OLDEST)
.observeOn(Schedulers.computation())
.subscribe(e -> {}, Throwable::printStackTrace);
Aqui, nossa estratégia para lidar com o buffer transbordando é descartar o elemento mais antigo em um buffer e adicionar o item mais novo produzido por umObservable.
Observe que as duas últimas estratégias causam uma descontinuidade no fluxo à medida que eliminam elementos. Além disso, eles não sinalizamBufferOverflowException.
7. Eliminando todos os elementos produzidos em excesso
Sempre que oObserver a jusante não estiver pronto para receber um elemento, podemos usar um métodoonBackpressureDrop() para remover esse elemento da sequência.
Podemos pensar nesse método como um métodoonBackpressureBuffer() com a capacidade de um buffer definido como zero com uma estratégiaON_OVERFLOW_DROP_LATEST.
Este operador é útil quando podemos ignorar com segurança os valores de uma fonteObservable (como movimentos do mouse ou sinais de localização GPS atuais), pois haverá valores mais atualizados posteriormente:
Observable.range(1, 1_000_000)
.onBackpressureDrop()
.observeOn(Schedulers.computation())
.doOnNext(ComputeFunction::compute)
.subscribe(v -> {}, Throwable::printStackTrace);
O métodoonBackpressureDrop() está eliminando um problema de superprodução deObservable, mas precisa ser usado com cuidado.
8. Conclusão
Neste artigo, examinamos um problema de superprodução deObservable e maneiras de lidar com uma contrapressão. Vimos estratégias de armazenamento em buffer, lote e pular elementos quandoObserver não é capaz de consumir elementos tão rapidamente quanto são produzidos por umObservable.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.