Intro to
Akka Streams & HTTP
Lance Arlaus
ny-scala
blog.lancearlaus.com
Going Reactive
quick take
-
Reactive is Real
- four tenets
- expanding options
-
Streams are Effective
- versatile abstraction
Intro to Akka Streams and HTTP
Overview
- Reactive Streams Basics
- concept: push-based streams
- innovation: demand-based flow
- Akka Streams Basics
- capability: reusable flow components
- abstraction: visual flow construction (DSL)
- Akka HTTP Basics
- improvement: fully stream-based
- Sample Service
What is a stream?
natural abstraction for sequenced data
(file, network, events, ...)
- has a beginning
- may be unbounded
- may be non-repeatable
Example | Bounded | Repeat | |
---|---|---|---|
(6, 7, 8, 9) | Fixed integer sequence | Yes | Yes |
(3, 4, 5, ...) | Infinite integer sequence | No | Yes |
(9, 3, 1, 6) | Fixed-length random sequence | Yes | No |
(7, 2, 4, ...) | Infinite random sequence | No | No |
(GET, ...) | Incoming HTTP requests | ? | ? |
What is the challenge?
mismatched producer and consumer
-
fast producer / slow consumer
- producer blocks (sync only)
- consumer drops
- slow producer / fast consumer
- consumer blocks e.g. iterator.next()
FAST (10/sec)
slooww (2/sec)
Robust Stream Processing
Reactive Streams
handle large data sets or rapid events with bounded resources
- data only flows downstream in response to demand
- all interfaces non-blocking
Akka Streams
reusable, natural expression of stream processing atop Akka
- data transformations
- flow graph components
Reactive Streams Basics
-
Publisher
-
Subscriber
-
Subscription
-
Processor
Publisher
-
how does data flow?
-
key concept: streams are push-based
-
-
data is never directly pulled from a Publisher
- no next() method
-
subscribe to Publisher, receive events (later) via Subscriber
Reactive Streams Basics
public interface Publisher<T> {
void subscribe(Subscriber<? super T> s);
}
Subscriber
-
asynchronous data events
- publisher.subscribe(subscriber)
- subscriber.onNext(element), ...
- subscriber.onComplete()
-
events pushed to subscriber
-
what about errors?
-
subscriber.onError(error)
-
Reactive Streams Basics
public interface Subscriber<T> {
void onSubscribe(Subscription s);
void onNext(T t);
void onError(Throwable t);
void onComplete();
}
how is this different than traditional event listeners or reactive extensions (Rx.NET, et al)?
Subscription
- deconstruct Iterator.next()
- signal demand (call implies demand)
- deliver data (return value)
- signal error (exception)
- data & error via subscriber.on(Next|Error)
- signal demand
-
- subscribe() implies demand? no
Reactive Streams Basics
Subscription
-
when does data flow?
- key innovation: flow is demand-based
- explicitly signal demand
-
-
subscription.request(count)
-
- no data events flow until demand is signaled
-
-
publisher.subscribe(subscriber)
-
subscriber.onSubscribe(subscription)
-
subscription.request(count)
-
subscriber.onNext(element), ...
-
Reactive Streams Basics
public interface Subscription {
void request(long n);
void cancel();
}
Processor
-
both Subscriber and Publisher
- processing stage (e.g. data transformation)
- non-terminal
Reactive Streams Basics
public interface Processor<T, R>
extends Subscriber<T>, Publisher<R> {
}
Reactive Streams Basics
Flow Visualization
the flow of demand and data
Summary
- key concept: streams are push-based
- key innovation: flow is demand-based
- Reactive Streams footprint
-
- Publisher
- Subscriber
- Subscription
- Processor
- async/non-blocking
Reactive Streams Basics
public interface Publisher<T> {
void subscribe(Subscriber<? super T> s);
}
public interface Subscriber<T> {
void onSubscribe(Subscription s);
void onNext(T t);
void onError(Throwable t);
void onComplete();
}
public interface Subscription {
void request(long n);
void cancel();
}
public interface Processor<T, R>
extends Subscriber<T>, Publisher<R> {
}
Streams: From Reactive to Akka
Reactive Streams
objective: minimal, well-specified integration API
- multi-vendor integration (part of Java 9?)
- not a user level API (really a SPI)
Akka Streams
objective: develop streaming applications
- component definition (source, sink, flow, etc.)
- transformation library (data, stream)
- graph construction (fan out/in, DSL)
-
- linear, branching, cyclic
- integration / customization
-
- Akka publisher / subscriber
- custom stages
|
Level1
(Basic)
|
Level 2
(Intermediate)
|
Level 3
(Advanced)
|
Concept
|
Stream
Graph, Shape, Inlet, Outlet
Materialization
|
Buffers
Stream of streams
Attributes
|
Cyclic Graphs
Recovery
|
Component
Shape Library
|
Source, Sink, Flow
|
Broadcast, Zip, ZipWith, Unzip, UnzipWith, Merge
|
FlexiRoute, FlexiMerge
BidiFlow
|
Transform
|
Data Transformation
|
Custom Transformation
Stream Transformation
Other
|
Custom Materialization
Stream Transformation
Error Handling
|
Construct
|
Linear Flows
DSL, Builder
|
Branching Flows
|
Cyclic Flows
Protocol Flows
|
Customize
|
N/A
|
|
|
Test
|
|
|
|
Akka Streams Topic Map
Today
Basic Building Blocks
- Source
- Sink
- Flow
Akka Streams Level 1 (Basics)
*conceptually
val source = Source(1 to 3)
val sum = Flow[Int].fold(0.0)(_ + _)
val sink = Sink.foreach[Double](println)
// Prints '6.0'
source.via(sum).to(sink).run
the streaming function
Function
Input
Output
Flow
Source
Sink
→
~>
~>
Publisher*
Processor*
Subscriber*
Inlet
→
Outlet
Shape: Inlets & Outlets
Akka Streams Level 1 (Basics)
Shape is to Graph as Signature is to Function
Function :
Graph :
Inputs & Outputs (Signature)
Inlets & Outlets (Shape)
val source: Source[Int] = Source(1 to 3)
val sum: Flow[Int, Double] = Flow[Int].fold(0.0)(_ + _)
val sink: Sink[Double] = Sink.foreach[Double](println)
// What is the shape of the following?
val runnable: RunnableGraph = source.via(sum).to(sink)
runnable.run
note: types intentionally simplified
Running a Graph
Akka Streams Level 1 (Basics)
implicit val system = ActorSystem("akka-streams")
implicit val materializer = ActorMaterializer()
// Create a runnable graph, steps omitted
val runnable: RunnableGraph = source.via(flow).to(sink)
// Run the graph with implicit Materializer
runnable.run()
Materializer is to Graph as
ExecutionContext is to Future
- Graph defines blueprint (akin to function def)
- Materializer runs a RunnableGraph (akin to function call)
- materialization allocates runtime resources
- Akka Streams uses Actors
- other (Spark, for example) theoretically possible
Materialized Value
Akka Streams Level 1 (Basics)
// Materialized type is the last type parameter by convention
// Sink that materializes a Future that completes when stream completes
val printer: Sink[Int, Future[Unit]] = Sink.foreach[Int](println)
// Sink that materializes a Future that completes with the first stream element
val head: Sink[Int, Future[Int]] = Sink.head[Int]
// Sources often don't materialize anything
val source: Source[Int, Unit] = Source(1 to 3)
// Source that emits periodically until cancelled via the materialized Cancellable
val ticks: Source[Int, Cancellable] = Source(1.second, 5.seconds, 42)
// Note that the above are merely blueprints
// No materialized values are produced until a graph is materialized
// Materialize a Graph which will run indefinitely or until cancelled
// Any graph can only materialize a single value
// Both printer and ticks materialize values (Future[Unit] and Cancellable)
// runWith() selects the target's materialized value
val cancellable: Cancellable = printer.runWith(ticks)
// Cancel the materialized ticks source
cancellable.cancel
Graph materialization result
- runtime resource produced by a Graph during materialization
- related to / used by processing, but not part of the stream itself
Concepts Checkpoint
Akka Streams Level 1 (Basics)
// Create flow materializer
implicit val system = ActorSystem("akka-streams")
implicit val materializer = ActorMaterializer()
// Create graph components
val nums = (1 to 10)
val source: Source[Int, Unit] = Source(nums)
val sum: Flow[Int, Int, Unit] = Flow[Int].fold(0)(_ + _)
val triple: Flow[Int, Int, Unit] = Flow[Int].map(_*3)
val head: Sink[Int, Future[Int]] = Sink.head[Int]
// Assemble and run a couple of graphs
val future1a: Future[Int] = source.via(sum).to(head).run
val future2a: Future[Int] = source.via(triple).via(sum).to(head).run
// Perform some basic tests
whenReady(future1a)(_ shouldBe nums.sum)
whenReady(future2a)(_ shouldBe (nums.sum * 3))
// Equivalent to the above graphs, using shortcuts for brevity
val future1b = Source(nums).runFold(0)(_ + _)
val future2b = Source(nums).via(triple).runFold(0)(_ + _)
Transformations
- map, mapAsync
- fold, scan, filter, collect
- take, drop
- take/drop(While|Within)
- grouped, groupedWithin
Akka Streams Level 1 (Basics)
the usual suspects...
// Source[Out, Mat]
def mapAsync[T](parallelism: Int)(f: (Out) ⇒ Future[T]): Source[T, Mat]
def takeWhile(p: (Out) ⇒ Boolean): Source[Out, Mat]
def takeWithin(d: FiniteDuration): Source[Out, Mat]
Transformations
- concat, concatMat
- flatten
- prefixAndTail, split(After|When)
- groupBy
- conflate, expand
Akka Streams Level 1 (Basics)
...and a few more
// Source[Out, Mat]
def prefixAndTail[U >: Out](n: Int): Source[(Seq[Out], Source[U, Unit]), Mat]
def splitWhen[U >: Out](p: (Out) ⇒ Boolean): Source[Source[U, Unit], Mat]
def groupBy[K, U >: Out](f: (Out) ⇒ K): Source[(K, Source[U, Unit]), Mat]
def conflate[S](seed: (Out) ⇒ S)(aggregate: (S, Out) ⇒ S): Source[S, Mat]
def expand[S, U](seed: (Out) ⇒ S)(extrapolate: (S) ⇒ (U, S)): Source[U, Mat]
Sample Application
Akka Streams & HTTP Level 1 (Basics)
enhanced historical price service
- calculate simple moving average (SMA)
- parse historical price CSV stream
- enhance historical price CSV stream
Part I (Streams)
- expose enhanced price service endpoint
- request historical prices via HTTP
- stream enhanced prices
Part II (HTTP)
Enhanced Price Service
Akka Streams Level 1 (Basics)
sample data
Date, Open, High, Low, Close, Volume, Adj Close
2014-12-31, 25.3, 25.3, 24.19, 24.84, 1438600, 24.84
2014-12-30, 26.28, 26.37, 25.29, 25.36, 766100, 25.36
2014-12-29, 26.64, 26.8, 26.13, 26.42, 619700, 26.42
2014-12-26, 27.25, 27.25, 26.42, 26.71, 360400, 26.71
Date, Open, High, Low, Close, Volume, Adj Close, Adj Close SMA(3)
2014-12-31, 25.3, 25.3, 24.19, 24.84, 1438600, 24.84, 25.54
2014-12-30, 26.28, 26.37, 25.29, 25.36, 766100, 25.36, 26.16
input
output
- reverse time series (most recent price first)
http://localhost:8080/stock/price/daily/yhoo?calculated=sma(3)
http://real-chart.finance.yahoo.com/table.csv?s=yhoo& ...
Enhanced Price Service
Akka Streams Level 1 (Basics)
- calculate
- enhance
- expose
Akka Streams Level 1 (Basics)
// Calculate simple moving average (SMA) using scan()
// to maintain a running sum and a sliding window
def sma(N: Int) = Flow[Double]
.scan((0.0, Seq.empty[Double])) {
case ((sum, win), x) =>
win.size match {
case N => (sum + x - win.head, win.tail :+ x)
case _ => (sum + x, win :+ x)
}
}
.drop(N) // Drop initial and incomplete windows
.map { case (sum, _) => sum / N.toDouble }
Source(1 to 5)
.map(n => (n*n).toDouble)
.via(sma(3))
.runForeach(sma => println(f"$sma%1.2f")
// Output:
// 4.67
// 9.67
// 16.67
Enhanced Price Stream
simple moving average (SMA)
Akka Streams Level 1 (Basics)
import akka.stream.io.Framing
// A CSV file row, parsed into columns
type Row = Array[String]
// Parse incoming bytes into CSV record stream
// Note: Each ByteString may contain more (or less) than one line
def parse(maximumLineLength: Int = 256): Flow[ByteString, Row, Unit] =
Framing.delimiter(ByteString("\n"), maximumLineLength, allowTruncation = true)
.map(_.utf8String.split("\\s*,\\s*"))
// Select a specific column (including header) by name
def select(name: String): Flow[Row, String, Unit] = Flow[Row]
.prefixAndTail(1).map { case (header, rows) =>
header.head.indexOf(name) match {
case -1 => Source.empty[String] // Named column not found
case index => Source.single(name)
.concatMat(rows.map(_(index)))(Keep.right)
}
}.flatten(FlattenStrategy.concat)
// Convert row into CSV formatted ByteString
val format = Flow[Row].map(row => ByteString(row.mkString("", ",", "\n")))
Enhanced Price Stream
CSV handling
Akka Streams Level 1 (Basics)
Enhanced Price Stream
SMA column
// Calculate and format SMA for a column, renaming the column
def smaCol(name: String, n: Int, format: String = "%1.2f") = Flow[String]
.prefixAndTail(1)
.map { case (header, data) =>
Source.single(name).concatMat(
data.map(_.toDouble)
.via(calculate.sma((n)))
.map(_.formatted(format))
)(Keep.right)
}
.flatten(FlattenStrategy.concat)
Adj Close
24.84
25.36
26.42
26.71
Adj Close SMA(3)
25.54
26.16
Branching Flows
Akka Streams Level 2 (Intermediate)
a common pattern
- branch stream (fan-out)
- transform each branch
- combine results (fan-in)
Fan-Out Shapes:
Fan-In Shapes:
Broadcast, Unzip, UnzipWith
Zip, ZipWith, Merge, MergePreferred
Graph DSL
Akka Streams Level 1 (Basics)
visual flow construction
bcast ~> ~> append.in0
bcast ~> select ~> smaCol ~> append.in1
// Calculate and append SMA column
def appendSma(n: Int): Flow[Row, Row, Unit] = Flow(
Broadcast[Row](2),
csv.select("Adj Close"),
smaCol(s"Adj Close SMA($n)", n),
ZipWith((row: Row, col: String) => row :+ col)
)((_, _, _, mat) => mat) {
implicit builder => (bcast, select, smaCol, append) =>
bcast ~> append.in0
bcast ~> select ~> smaCol ~> append.in1
(bcast.in, append.out)
}
appendSma
Akka Streams Level 1 (Basics)
import akka.stream.io._
val inSource = SynchronousFileSource(new File("input.csv"))
val expSource = SynchronousFileSource(new File("expected.csv"))
val builder = new ByteStringBuilder()
val outSink = Sink.foreach[ByteString](builder ++= _)
val outSource = Source(() => Iterator.single(builder.result()))
val window = 3
val smaName = s"Adj Close SMA($window)"
val future = inSource.via(csv.parse()).via(quote.appendSma(window)).via(csv.format)
.runWith(outSink)
whenReady(future) { unit =>
// Compare SMA column from output and expected
val selectSma = csv.parse().via(csv.select(smaName)).drop(1).map(_.toDouble)
val outFuture = outSource.via(selectSma).runFold(List.empty[Double])(_ :+ _)
val expFuture = expSource.via(selectSma).runFold(List.empty[Double])(_ :+ _)
whenReady(Future.sequence(Seq(outFuture, expFuture))) { case out :: exp :: Nil =>
out should have size exp.size
out.zip(exp).foreach { case (out, exp) =>
out shouldBe exp
}
}
}
Enhanced Price Stream
testing
Sample Application
Akka HTTP Level 1 (Basics)
enhanced historical price service
- calculate simple moving average (SMA)
- parse historical price CSV stream
- enhance historical price CSV stream
Part I (Streams)
- expose enhanced price service endpoint
- request historical prices via HTTP
- stream enhanced prices
Part II (HTTP)
Introducing Akka HTTP
stream-based web services
- "Spray 2.0" - address weaknesses and polish features
- key improvement: fully stream-based
- easily handle chunked responses and large entities
- address missing features (WebSockets, anyone?)
- reusable streams transformations
- extensible HTTP model and spec implementation
Akka HTTP Level 1 (Basics)
From Akka Streams to HTTP
a natural mapping
in
out
Akka HTTP Level 1 (Basics)
Akka HTTP
fully stream-based
// TCP and HTTP protocols modeled as Flows
// Flow is materialized for each incoming connection
// TCP
Tcp().bindAndHandle(handler: Flow[ByteString, ByteString, _], ...)
// HTTP (server low-level)
// Q: How is HTTP pipelining supported?
Http().bindAndHandle(handler: Flow[HttpRequest, HttpResponse, Any], ...)
// HTTP (client low-level)
Http().outgoingConnection(host: String, port: Int = 80, ...):
Flow[HttpRequest, HttpResponse, Future[OutgoingConnection]]
// HTTP entities modeled using ByteString Sources
// Request entity modeled as a source of bytes (Source[ByteString])
val request: HttpRequest
val dataBytes: Source[ByteString, Any] = request.entity.dataBytes
// Use Source[ByteString] to create response entity
val textSource: Source[ByteString, Any]
val chunked = HttpEntity.Chunked.fromData(MediaTypes.`text/plain`, textSource)
val response = HttpResponse(entity = chunked)
Akka HTTP Level 1 (Basics)
Enhanced Price Service
Akka HTTP Level 1 (Basics)
mock service endpoint
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model.MediaTypes._
import akka.stream.io.SynchronousFileSource
// Find mock data file (if it exists) for the given symbol
def mockFile(symbol: String): Option[File] =
Option(getClass.getResource(s"/mock/stock/price/$symbol.csv"))
.map(url => new File(url.getFile))
.filter(_.exists)
// http://localhost/stock/price/daily/yhoo
val route: Route =
(get & path("stock"/"price"/"daily" / Segment)) { (symbol) =>
complete {
mockFile(symbol) match {
// Create chunked response from Source[ByteString]
case Some(file) => HttpEntity.Chunked.fromData(`text/csv`, SynchronousFileSource(file))
case None => NotFound
}
}
}
// Run a server with the given route (implicit conversion to Flow[HttpRequest, HttpResponse])
val binding = Http().bindAndHandle(route, "localhost", 8080)
Enhanced Price Service
Akka HTTP Level 1 (Basics)
fetching data
import csv.Row
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpEntity
import akka.http.scaladsl.model.MediaTypes._
import akka.http.scaladsl.server.Directives._
trait StockPriceClient {
def history(symbol: String): Future[Either[(StatusCode, String), Source[Row, Any]]]
}
case class YahooStockPriceClient
(implicit system: ActorSystem, executor: ExecutionContextExecutor, materializer: Materializer)
extends StockPricesClient
{
override def history(symbol: String) = {
val uri = buildUri(symbol) // http://real-chart.finance.yahoo.com/table.csv?s=$symbol&...
Http().singleRequest(RequestBuilding.Get(uri)).map { response =>
response.status match {
case OK => Right(response.entity.dataBytes.via(csv.parse())
case NotFound => Left(NotFound -> s"No data found for $symbol")
case status => Left(status -> s"Request to $uri failed with status $status")
}
}
}
}
Enhanced Price Service
Akka HTTP Level 1 (Basics)
expose service endpoint
import csv.Row
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpEntity
import akka.http.scaladsl.model.MediaTypes._
import akka.http.scaladsl.server.Directives._
trait StockPriceClient {
def history(symbol: String): Future[Either[(StatusCode, String), Source[Row, Any]]]
}
val client: StockPriceClient
// http://localhost/stock/prices/daily/yhoo/sma(10)
val route: Route =
(get & path("stock"/"prices"/"daily" / Segment / "sma(" ~ IntValue ~ ")")) {
(symbol, window) =>
client.history(symbol).map[ToResponseMarshallable] {
case Right(source) => HttpEntity.Chunked.fromData(`text/csv`,
source.via(quote.appendSma(window)).via(csv.format))
case Left(err @ (NotFound, _)) => err
case Left(_) => ServiceUnavailable -> "Error calling underlying service"
}
}
Enhanced Price Service
Akka Streams Level 1 (Basics)
solution review
Full Sample
Intro to Akka Streams & HTTP
https://github.com/lancearlaus/akka-streams-http-intro
- Bitcoin trades OHLCV service
- stackable services
- WebSockets
- custom stage
- custom route segments / parameters
- flow graph packaging
more to explore
⇒ git clone https://github.com/lancearlaus/akka-streams-http-introduction
⇒ sbt run
[info] Running Main
Starting server on localhost:8080...STARTED
Get started with the following URLs:
Stock Price Service:
Yahoo with default SMA : http://localhost:8080/stock/price/daily/yhoo
Yahoo 2 years w/ SMA(200) : http://localhost:8080/stock/price/daily/yhoo?period=2y&calculated=sma(200)
Facebook 1 year raw history : http://localhost:8080/stock/price/daily/fb?period=1y&raw=true
Bitcoin Trades Service:
Hourly OHLCV (Bitstamp USD) : http://localhost:8080/bitcoin/price/hourly/bitstamp/USD
Daily OHLCV (itBit USD) : http://localhost:8080/bitcoin/price/daily/itbit/USD
Recent trades (itBit USD) : http://localhost:8080/bitcoin/trades/itbit/USD
Trades raw response : http://localhost:8080/bitcoin/trades/bitstamp/USD?raw=true
Intro to Akka Streams and HTTP
Summary
- Reactive Streams Basics
- Akka Streams Basics
- Akka HTTP Basics
- Sample Service
Questions?
Intro to Akka Streams and HTTP
By Lance Arlaus
Intro to Akka Streams and HTTP
Got Streams? Streams are an effective way to model many real-world data problems from event processing to the typical service request/response cycle. Learn the basics of Akka Streams & HTTP through a practical introductory training talk, no prior Streams experience required. The talk will cover Akka Streams from the ground up, starting with Reactive Streams concepts, to give participants a solid foundation. Akka Streams & HTTP building blocks and capabilities will be explained and showcased via a stream processing example exposed as an Akka HTTP service.
- 5,616