Guia para córregos Akka

Guia para córregos Akka

1. Visão geral

Neste artigo, veremos a bibliotecaakka-streams que é construída sobre a estrutura do ator Akka, que seguereactive streams manifesto. The Akka Streams API allows us to easily compose data transformation flows from independent steps.

Além disso, todo o processamento é feito de maneira reativa, sem bloqueio e assíncrona.

2. Dependências do Maven

Para começar, precisamos adicionar as bibliotecasakka-streameakka-stream-testkit em nossopom.xml:


    com.typesafe.akka
    akka-stream_2.11
    2.5.2


    com.typesafe.akka
    akka-stream-testkit_2.11
    2.5.2

3. API Akka Streams

Para trabalhar com o Akka Streams, precisamos estar cientes dos principais conceitos da API:

  • Source the entry point to processing in the akka-stream library - podemos criar uma instância desta classe a partir de várias fontes; por exemplo, podemos usar o métodosingle() se quisermos criar umSource de um únicoString, ou podemos criar umSource de umIterable de elementos

  • Flow – the main processing building block - cada instânciaFlow tem um valor de entrada e um valor de saída

  • Materializer – we can use one if we want our Flow to have some side effects like logging or saving results; mais comumente, passaremos o aliasNotUsed comoMaterializer para denotar que nossoFlow não deve ter nenhum efeito colateral

  • Sink operation – when we are building a Flow, it is not executed until we will register a Sink operation nele - é uma operação terminal que dispara todos os cálculos em todo oFlow

4. Criação deFlows em fluxos Akka

Vamos começar construindo um exemplo simples, onde mostraremos comocreate and combine multiple Flows - para processar um fluxo de inteiros e calcular a janela móvel média de pares inteiros do fluxo.

Analisaremos umString delimitado por ponto e vírgula de inteiros como entrada para criar nossoakka-stream Source para o exemplo.

4.1. Usando umFlow para analisar a entrada

Primeiro, vamos criar uma classeDataImporter que terá uma instância deActorSystem que usaremos mais tarde para criar nossoFlow:

public class DataImporter {
    private ActorSystem actorSystem;

    // standard constructors, getters...
}

A seguir, vamos criar um métodoparseLine que irá gerar umList deInteger a partir de nossa entrada delimitadaString.. Lembre-se de que estamos usando a API Java Stream aqui apenas para análise:

private List parseLine(String line) {
    String[] fields = line.split(";");
    return Arrays.stream(fields)
      .map(Integer::parseInt)
      .collect(Collectors.toList());
}

NossoFlow inicial aplicaráparseLine à nossa entrada para criar umFlow com tipo de entradaStringe tipo de saídaInteger:

private Flow parseContent() {
    return Flow.of(String.class)
      .mapConcat(this::parseLine);
}

Quando chamamos o métodoparseLine(), o compilador sabe que o argumento para aquela função lambda seráString - o mesmo que o tipo de entrada para nossoFlow.

Observe que estamos usando o métodomapConcat() - equivalente ao método Java 8flatMap() - porque queremos nivelar oList deInteger retornado porparseLine() em aFlow deInteger para que as etapas subsequentes em nosso processamento não precisem lidar comList.

4.2. Usando umFlow para realizar cálculos

Neste ponto, temos nossoFlow de inteiros analisados. Agora, precisamosimplement logic that will group all input elements into pairs and calculate an average of those pairs.

Agora, vamoscreate a Flow of Integers and group them using the grouped() method.

Em seguida, queremos calcular uma média.

Como não estamos interessados ​​na ordem em que essas médias serão processadas, podemoshave averages calculated in parallel using multiple threads by using the mapAsyncUnordered() method, passando o número de threads como um argumento para este método.

A ação que será passada como lambda paraFlow precisa retornar umCompletableFuture porque essa ação será calculada de forma assíncrona no thread separado:

private Flow computeAverage() {
    return Flow.of(Integer.class)
      .grouped(2)
      .mapAsyncUnordered(8, integers ->
        CompletableFuture.supplyAsync(() -> integers.stream()
          .mapToDouble(v -> v)
          .average()
          .orElse(-1.0)));
}

Estamos calculando médias em oito segmentos paralelos. Observe que estamos usando a API Java 8 Stream para calcular uma média.

4.3. Compondo váriosFlows em um únicoFlow

A APIFlow é uma abstração fluente que nos permitecompose multiple Flow instances to achieve our final processing goal. Podemos ter fluxos granulares onde um, por exemplo, está analisandoJSON,, outro está fazendo alguma transformação e outro está coletando algumas estatísticas.

Essa granularidade nos ajudará a criar um código mais testável porque podemos testar cada etapa do processamento de forma independente.

Criamos dois fluxos acima que podem funcionar independentemente um do outro. Agora, queremos compor eles juntos.

Primeiro, queremos analisar nossa entradaString e, a seguir, queremos calcular uma média em um fluxo de elementos.

Podemos compor nossos fluxos usando o métodovia():

Flow calculateAverage() {
    return Flow.of(String.class)
      .via(parseContent())
      .via(computeAverage());
}

Criamos umFlow tendo o tipo de entradaStringe dois outros fluxos depois dele. OparseContent()Flow pega uma entradaStringe retorna umInteger como saída. OcomputeAverage() Flow está pegando esseIntegere calcula uma média de retornoDouble como o tipo de saída.

5. AdicionandoSink aoFlow

Como mencionamos, até este ponto oFlow inteiro ainda não foi executado porque é preguiçoso. To start execution of the Flow we need to define a Sink. A operaçãoSink pode, por exemplo, salvar dados em um banco de dados ou enviar resultados para algum serviço da web externo.

Suponha que temos uma classeAverageRepository com o seguinte métodosave() que grava os resultados em nosso banco de dados:

CompletionStage save(Double average) {
    return CompletableFuture.supplyAsync(() -> {
        // write to database
        return average;
    });
}

Agora, queremos criar uma operaçãoSink que use este método para salvar os resultados do nosso processamentoFlow. Para criar nossoSink,, primeiro precisamoscreate a Flow that takes a result of our processing as the input type. Em seguida, queremos salvar todos os nossos resultados no banco de dados.

Novamente, não nos importamos com a ordem dos elementos, portanto, podemosperform the save() operations in parallel usando o métodomapAsyncUnordered().

Para criar umSink a partir deFlow, precisamos chamartoMat() comSink.ignore() como primeiro argumento eKeep.right() como o segundo porque queremos retornar um estado do processamento:

private Sink> storeAverages() {
    return Flow.of(Double.class)
      .mapAsyncUnordered(4, averageRepository::save)
      .toMat(Sink.ignore(), Keep.right());
}

6. Definindo uma fonte paraFlow

A última coisa que precisamos fazer écreate a Source from the input*String*. Podemos aplicarcalculateAverage()Flow a esta fonte usando o métodovia().

Então, para adicionarSink ao processamento, precisamos chamar o métodorunWith()e passar ostoreAverages() Sink que acabamos de criar:

CompletionStage calculateAverageForContent(String content) {
    return Source.single(content)
      .via(calculateAverage())
      .runWith(storeAverages(), ActorMaterializer.create(actorSystem))
      .whenComplete((d, e) -> {
          if (d != null) {
              System.out.println("Import finished ");
          } else {
              e.printStackTrace();
          }
      });
}

Observe que, quando o processamento é concluído, estamos adicionando o retorno de chamadawhenComplete(), no qual podemos realizar alguma ação dependendo do resultado do processamento.

7. TestandoAkka Streams

Podemos testar nosso processamento usando oakka-stream-testkit.

A melhor maneira de testar a lógica real do processamento é testar toda a lógicaFlowe usarTestSink para acionar o cálculo e confirmar os resultados.

Em nosso teste, estamos criando oFlow que queremos testar e, a seguir, estamos criando umSource a partir do conteúdo de entrada de teste:

@Test
public void givenStreamOfIntegers_whenCalculateAverageOfPairs_thenShouldReturnProperResults() {
    // given
    Flow tested = new DataImporter(actorSystem).calculateAverage();
    String input = "1;9;11;0";

    // when
    Source flow = Source.single(input).via(tested);

    // then
    flow
      .runWith(TestSink.probe(actorSystem), ActorMaterializer.create(actorSystem))
      .request(4)
      .expectNextUnordered(5d, 5.5);
}

Estamos verificando se estamos esperando quatro argumentos de entrada, e dois resultados que são médias podem chegar em qualquer ordem, porque nosso processamento é feito da maneira assíncrona e paralela.

8. Conclusão

Neste artigo, examinamos a bibliotecaakka-stream.

Definimos um processo que combina váriosFlows para calcular a média móvel dos elementos. Em seguida, definimos umSource que é um ponto de entrada do processamento do fluxo e umSink que dispara o processamento real.

Finalmente, escrevemos um teste para nosso processamento usandoakka-stream-testkit.

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á.