Skip to content

Commit 7a30a5d

Browse files
authored
Merge pull request #2 from StreetContxt/add-stats
Added Stats
2 parents 3781129 + 3579710 commit 7a30a5d

File tree

6 files changed

+110
-23
lines changed

6 files changed

+110
-23
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,20 @@ A lightweight Scala wrapper around Kinesis Producer Library (KPL).
44
The main benefit of this library is working with Scala-native Futures when
55
interacting with KPL.
66

7+
8+
## Amazon Licensing Restrictions
9+
**KPL license is not compatible with open source licenses!** See
10+
[this discussion](https://issues.apache.org/jira/browse/LEGAL-198) for more details.
11+
12+
As such, the licensing terms of this library is Apache 2 license **PLUS** whatever restrictions
13+
are imposed by the KPL license.
14+
15+
716
## No Message Ordering
817
Kinesis producer library **does not provide message ordering guarantees** at a reasonable throughput,
918
see [this ticket](https://github.com/awslabs/amazon-kinesis-producer/issues/23) for more details.
1019

20+
1121
## Integration Tests
1222
This library is tested as part of [kcl-akka-stream](https://github.com/StreetContxt/kcl-akka-stream)
1323
integration tests.

build.sbt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
organization in ThisBuild := "com.contxt"
22
scalaVersion in ThisBuild := "2.11.8"
3+
version in ThisBuild := "1.0.0-SNAPSHOT"
34

45
val slf4j = "org.slf4j" % "slf4j-api" % "1.7.21"
56
val amazonKinesisProducer = "com.amazonaws" % "amazon-kinesis-producer" % "0.12.8"
7+
val typesafeConfig = "com.typesafe" % "config" % "1.3.1"
68

79
libraryDependencies ++= Seq(
810
slf4j,
9-
amazonKinesisProducer
11+
amazonKinesisProducer,
12+
typesafeConfig
1013
)

src/main/resources/reference.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
com.contxt.kinesis {
2+
producer {
3+
stats-class-name = "com.contxt.kinesis.NoopProducerStats"
4+
}
5+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.contxt.kinesis
2+
3+
import com.amazonaws.services.kinesis.producer.UserRecordResult
4+
import com.typesafe.config.{ Config, ConfigFactory }
5+
import org.slf4j.LoggerFactory
6+
import scala.concurrent.Future
7+
import scala.util.control.NonFatal
8+
9+
trait ProducerStats {
10+
def trackSend(streamId: StreamId, size: Int)(closure: => Future[UserRecordResult]): Future[UserRecordResult]
11+
def reportShutdown(streamId: StreamId): Unit
12+
}
13+
14+
object ProducerStats {
15+
private val log = LoggerFactory.getLogger(classOf[ProducerStats])
16+
17+
def getInstance(config: Config): ProducerStats = {
18+
try {
19+
val className = config.getString("com.contxt.kinesis.producer.stats-class-name")
20+
Class.forName(className).newInstance().asInstanceOf[ProducerStats]
21+
}
22+
catch {
23+
case NonFatal(e) =>
24+
log.error("Could not load a `ProducerStats` instance, falling back to `NoopProducerStats`.", e)
25+
new NoopProducerStats
26+
}
27+
}
28+
}
29+
30+
class NoopProducerStats extends ProducerStats {
31+
def trackSend(streamId: StreamId, size: Int)(closure: => Future[UserRecordResult]): Future[UserRecordResult] = closure
32+
def reportShutdown(streamId: StreamId): Unit = {}
33+
}

src/main/scala/com/contxt/kinesis/ScalaKinesisProducer.scala

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,68 +2,95 @@ package com.contxt.kinesis
22

33
import com.amazonaws.services.kinesis.producer.{ KinesisProducer, KinesisProducerConfiguration, UserRecordResult }
44
import com.google.common.util.concurrent.ListenableFuture
5+
import com.typesafe.config.{ Config, ConfigFactory }
56
import java.nio.ByteBuffer
67
import scala.concurrent._
78
import scala.language.implicitConversions
89
import scala.util.Try
10+
import scala.collection.JavaConversions._
11+
import scala.concurrent.ExecutionContext.Implicits.global
912

1013
/** A lightweight Scala wrapper around Kinesis Producer Library (KPL). */
1114
trait ScalaKinesisProducer {
1215

16+
def streamId: StreamId
17+
1318
/** Sends a record to a stream. See
1419
* [[[com.amazonaws.services.kinesis.producer.KinesisProducer.addUserRecord(String, String, String, ByteBuffer):ListenableFuture[UserRecordResult]*]]].
1520
*/
1621
def send(partitionKey: String, data: ByteBuffer, explicitHashKey: Option[String] = None): Future[UserRecordResult]
1722

18-
/** Flushes all the outgoing messages, returning a Future that completes when all the flushed messages have been sent.
19-
* See [[com.amazonaws.services.kinesis.producer.KinesisProducer.flushSync]].
20-
*/
21-
def flushAll(): Future[Unit]
22-
2323
/** Performs an orderly shutdown, waiting for all the outgoing messages before destroying the underlying producer. */
2424
def shutdown(): Future[Unit]
2525
}
2626

2727
object ScalaKinesisProducer {
28-
def apply(streamName: String, producerConfig: KinesisProducerConfiguration): ScalaKinesisProducer = {
29-
val producer = new KinesisProducer(producerConfig)
30-
new ScalaKinesisProducerImpl(streamName, producer)
28+
def apply(
29+
streamName: String,
30+
kplConfig: KinesisProducerConfiguration,
31+
config: Config = ConfigFactory.load()
32+
): ScalaKinesisProducer = {
33+
val producerStats = ProducerStats.getInstance(config)
34+
ScalaKinesisProducer(streamName, kplConfig, producerStats)
35+
}
36+
37+
def apply(
38+
streamName: String,
39+
kplConfig: KinesisProducerConfiguration,
40+
producerStats: ProducerStats
41+
): ScalaKinesisProducer = {
42+
val streamId = StreamId(kplConfig.getRegion, streamName)
43+
val producer = new KinesisProducer(kplConfig)
44+
new ScalaKinesisProducerImpl(streamId, producer, producerStats)
3145
}
3246

3347
private[kinesis] implicit def listenableToScalaFuture[A](listenable: ListenableFuture[A]): Future[A] = {
34-
implicit val executionContext: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global
3548
val promise = Promise[A]
3649
val callback = new Runnable {
3750
override def run(): Unit = promise.tryComplete(Try(listenable.get()))
3851
}
39-
listenable.addListener(callback, executionContext)
52+
listenable.addListener(callback, global)
4053
promise.future
4154
}
4255
}
4356

4457
private[kinesis] class ScalaKinesisProducerImpl(
45-
val streamName: String,
46-
private val producer: KinesisProducer
58+
val streamId: StreamId,
59+
private val producer: KinesisProducer,
60+
private val stats: ProducerStats
4761
) extends ScalaKinesisProducer {
4862
import ScalaKinesisProducer.listenableToScalaFuture
4963

5064
def send(partitionKey: String, data: ByteBuffer, explicitHashKey: Option[String]): Future[UserRecordResult] = {
51-
producer.addUserRecord(streamName, partitionKey, explicitHashKey.orNull, data)
52-
}
53-
54-
def flushAll(): Future[Unit] = {
55-
import scala.concurrent.ExecutionContext.Implicits.global
56-
Future {
57-
blocking {
58-
producer.flushSync()
65+
stats.trackSend(streamId, data.remaining) {
66+
producer.addUserRecord(streamId.streamName, partitionKey, explicitHashKey.orNull, data).map { result =>
67+
if (!result.isSuccessful) throwSendFailedException(result) else result
5968
}
6069
}
6170
}
6271

6372
def shutdown(): Future[Unit] = {
64-
import scala.concurrent.ExecutionContext.Implicits.global
6573
val allFlushedFuture = flushAll()
66-
allFlushedFuture.onComplete(_ => producer.destroy())
74+
allFlushedFuture.onComplete { _ =>
75+
producer.destroy()
76+
stats.reportShutdown(streamId)
77+
}
6778
allFlushedFuture
6879
}
80+
81+
private def throwSendFailedException(result: UserRecordResult): Nothing = {
82+
val attemptCount = result.getAttempts.size
83+
val errorMessage = result.getAttempts.lastOption.map(_.getErrorMessage)
84+
throw new RuntimeException(
85+
s"Sending a record to $streamId failed after $attemptCount attempts, last error message: $errorMessage."
86+
)
87+
}
88+
89+
private def flushAll(): Future[Unit] = {
90+
Future {
91+
blocking {
92+
producer.flushSync()
93+
}
94+
}
95+
}
6996
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.contxt.kinesis
2+
3+
case class StreamId(
4+
/** AWS region name. */
5+
regionName: String,
6+
7+
/** Stream name. */
8+
streamName: String
9+
)

0 commit comments

Comments
 (0)