Akka Streamsへのガイド

1概要

この記事では、Akkaアクターフレームワークの上に構築されているhttp://doc.akka.io/docs/akka/current/scala/stream/[ akka-streams ]ライブラリを調べます。 リアクティブストリームのマニフェスト 。 Akka Streams APIを使用すると、独立したステップからデータ変換フローを簡単に作成できます。

さらに、すべての処理は反応的、非ブロッキング、そして非同期的な方法で行われます。

2 Mavenの依存関係

始めるには、 https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22com.typesafe.akka%22%20AND%20a%3A%22akka-stream 2を追加する必要があります。 11%21 -testkit 2.11%22[ akka-stream-testkit ]ライブラリを pom.xmlに追加します。__

<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-stream__2.11</artifactId>
    <version>2.5.2</version>
</dependency>
<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-stream-testkit__2.11</artifactId>
    <version>2.5.2</version>
</dependency>

3 Akka Streams API

Akka Streamsを使用するには、コアAPIの概念を知っておく必要があります。

  • ** __http://doc.akka.io/japi/akka/2.5.2/akka/stream/scaladsl/Source.html[ソース]

    • akak-stream ライブラリ内の処理への入り口** - 複数のソースからこのクラスのインスタンスを作成します。たとえば、 から Source を作成したい場合は single() メソッドを使用できます。 単一の String 、または Iterable ofから Source を作成できます。 要素

  • ** Flow

    • メイン処理のビルディングブロック** - すべての Flow インスタンスには1つのインスタンスがあります。 入力値と1つの出力値

  • ** __マテリアライザー - 私たちの Flow にいくつかを持たせたい場合、w __eは1つを使用できます

ロギングや結果の保存などの副作用 。最も一般的には、 NotUsed エイリアスを Materializer として渡して、 Flow に副作用があってはいけません ** Sink

操作 - Flowを構築しているとき、 httpを登録するまでそれは実行されません。それにoperation ** - Flow 全体ですべての計算を引き起こすのは端末操作です。

4 Akka Streamsで Flows を作成する

簡単な例を作ることから始めましょう。ここでは、整数のストリームを処理し、そのストリームから整数ペアの平均移動ウィンドウを計算する方法を説明します。

例として akka-stream Source を作成するために、入力としてセミコロンで区切られた String の整数を解析します。

4.1. Flow を使って入力を解析する

まず、 Flow を作成するために後で使用する ActorSystem のインスタンスを取得する DataImporter クラスを作成しましょう。

public class DataImporter {
    private ActorSystem actorSystem;

   //standard constructors, getters...
}

次に、区切られた入力 Stringから Integer List を生成する parseLine__メソッドを作成しましょう。ここでは、解析にのみJava Stream APIを使用していることに注意してください。

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

最初の Flow は、入力タイプ String と出力タイプ Integer を持つ Flow を作成するために、入力に parseLine を適用します。

private Flow<String, Integer, NotUsed> parseContent() {
    return Flow.of(String.class)
      .mapConcat(this::parseLine);
}

parseLine() メソッドを呼び出すと、そのラムダ関数の引数が String になることがコンパイラによって認識されます。

mapConcat() メソッド(Java 8の flatMap() メソッドと同等)を使用していることに注意してください。これは、以降の処理のステップを可能にするために、 parseLine() によって返される List of Integer Flow に統合するためです。 List を処理する必要はありません。

4.2. Flow を使って計算を実行する

この時点で、私たちは解析された整数の Flow を持っています。さて、すべての入力要素をペアにグループ化し、それらのペアの平均を計算するロジックを実装する必要があります。

これで、 Flow __Integer sを作成し、 grouped()__メソッドを使用してそれらをグループ化します。

次に、平均を計算します。

これらの平均値が処理される順序には興味がないので、 mapAsyncUnordered() メソッドを使用して このメソッドに引数としてスレッド数を渡して 複数のスレッドを使用して並列に平均値を計算できます。

ラムダとして Flow に渡されるアクションは CompletableFuture を返す必要があります。そのアクションは別のスレッドで非同期に計算されるためです。

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

8つの並列スレッドで平均を計算しています。平均を計算するためにJava 8 Stream APIを使用していることに注意してください。

4.3. 複数の Flows を単一の Flow に合成する

Flow APIは流暢な抽象化で、最終的な処理目標を達成するために 複数の Flow インスタンスを作成することができます 。たとえば、1つが__JSONを解析し、もう1つが何らかの変換を行い、もう1つが何らかの統計を収集しているような細かいフローを作成できます。

このような粒度は、各処理ステップを独立してテストすることができるため、よりテストしやすいコードを作成するのに役立ちます。

上記の2つのフローを互いに独立して機能するように作成しました。

今、私たちはそれらを一緒に作曲したいのです。

まず、入力 String を解析し、次に、一連の要素の平均を計算します。

via() メソッドを使ってフローを構成することができます。

Flow<String, Double, NotUsed> calculateAverage() {
    return Flow.of(String.class)
      .via(parseContent())
      .via(computeAverage());
}

入力タイプ String とそれに続く他の2つのフローを持つ Flow を作成しました。 parseContent() Flow は、 String 入力を受け取り、 Integer を出力として返します。 computeAverage()Flow はその Integer を取り、出力タイプとして Double を返す平均を計算します。

5 Flow Sink を追加する

すでに述べたように、この時点では Flow 全体は遅延しているためまだ実行されていません。 Flow の実行を開始するには、 Sink を定義する必要があります。 Sink 操作では、たとえば、データをデータベースに保存したり、外部のWebサービスに結果を送信したりできます。

データベースに結果を書き込む次の save() メソッドを持つ AverageRepository クラスがあるとします。

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

それでは、このメソッドを使用して Flow 処理の結果を保存する Sink 操作を作成します。 Sinkを作成するには、まず、入力タイプとして処理の結果を受け取る Flow__を作成する必要があります。次に、すべての結果をデータベースに保存します。

繰り返しになりますが、要素の順序は気にしないので、 mapAsyncUnordered()メソッドを使用して save()操作を並行して実行できます。

Flow から Sink を作成するには、最初の引数として Sink.ignore() を、2番目として Keep.right() を指定して toMat() を呼び出す必要があります。処理のステータスを返すためです。

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

6. Flow のソースを定義する

最後にやるべきことは、 入力 String から Source を作成することです。 via() メソッドを使って、このソースに calculateAverage() Flow__を適用することができます。

次に、 Sink を処理に追加するには、 runWith() メソッドを呼び出して、作成したばかりの storeAverages()Sink を渡す必要があります。

CompletionStage<Done> 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();
          }
      });
}

処理が終了したら、 whenComplete() コールバックを追加します。これにより、処理の結果に応じて何らかのアクションを実行できます。

7. テスト Akka Streams

__akka-stream-testkitを使用して処理をテストできます。

  • 実際の処理ロジックをテストする最良の方法は、すべての Flow ロジックをテストし、 TestSink を使用して計算を開始し、結果を表明することです。**

このテストでは、テストする Flow を作成し、次にテスト入力コンテンツから Source を作成します。

@Test
public void givenStreamOfIntegers__whenCalculateAverageOfPairs__thenShouldReturnProperResults() {
   //given
    Flow<String, Double, NotUsed> tested = new DataImporter(actorSystem).calculateAverage();
    String input = "1;9;11;0";

   //when
    Source<Double, NotUsed> flow = Source.single(input).via(tested);

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

私たちは4つの入力引数を期待していることをチェックしています、そして、平均である2つの結果は非同期で並列な方法で行われるのでどんな順序でも到着することができます。

8結論

この記事では、 akka-stream ライブラリーを見ていました。

要素の移動平均を計算するために複数の Flows を組み合わせるプロセスを定義しました。次に、ストリーム処理のエントリポイントである Source と実際の処理をトリガーする Sink を定義しました。

最後に、 akka-stream-testkit を使用して処理のテストを書きました。

これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/akka-streams[GitHubプロジェクト]にあります - これはMavenプロジェクトなので、簡単にできます。そのままインポートして実行します。