Skip to content

Commit 2938624

Browse files
plaflammedangerousben
authored andcommitted
Return rows as an AsyncStream instead of buffering. (#128)
* Return rows as an `AsyncStream` instead of buffering. This allows streaming large result sets instead of bufferring them. The change is fairly invasive because the state machine has to be adapted to allow returning a `PgResponse` before the client is allowed to dispatch other requests on the connection. This is handled in the dispatcher where it expects such `PgResponse`s to provide a signal to release the connection.
1 parent 4024240 commit 2938624

File tree

17 files changed

+238
-123
lines changed

17 files changed

+238
-123
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/). Note that Semantic Versioning is not
55
necessarily followed during pre-1.0 development.
66

7+
## <Next release>
8+
9+
* Select results can now be streamed as `AsyncStream[DataRow]` and result sets as `AsyncStream[Row]`
10+
711
## 0.8.2
812
* Fix SSL session verification.
913
* Fix #75 - Name resolution failed

finagle-postgres-shapeless/src/main/scala/com/twitter/finagle/postgres/generic/Query.scala

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
package com.twitter.finagle.postgres.generic
22

3-
import scala.collection.immutable.Queue
3+
import com.twitter.concurrent.AsyncStream
44

5+
import scala.collection.immutable.Queue
56
import com.twitter.finagle.postgres.{Param, PostgresClient, Row}
67
import com.twitter.util.Future
8+
79
import scala.language.existentials
810

911
case class Query[T](parts: Seq[String], queryParams: Seq[QueryParam], cont: Row => T) {
10-
def run(client: PostgresClient): Future[Seq[T]] = {
12+
13+
def stream(client: PostgresClient): AsyncStream[T] = {
1114
val (queryString, params) = impl
12-
client.prepareAndQuery[T](queryString, params: _*)(cont)
15+
client.prepareAndQueryToStream[T](queryString, params: _*)(cont)
1316
}
1417

18+
def run(client: PostgresClient): Future[Seq[T]] =
19+
stream(client).toSeq
20+
1521
def exec(client: PostgresClient): Future[Int] = {
1622
val (queryString, params) = impl
1723
client.prepareAndExecute(queryString, params: _*)

finagle-postgres-shapeless/src/test/scala/com/twitter/finagle/postgres/generic/QuerySpec.scala

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.twitter.finagle.postgres.generic
22

33
import java.nio.charset.Charset
44

5+
import com.twitter.concurrent.AsyncStream
56
import com.twitter.finagle.Status
67
import com.twitter.finagle.postgres.messages.SelectResult
78
import com.twitter.finagle.postgres._
@@ -18,27 +19,27 @@ class QuerySpec extends FreeSpec with Matchers with MockFactory {
1819
val row = mock[Row]
1920

2021
trait MockClient {
21-
def prepareAndQuery[T](sql: String, params: List[Param[_]], f: Row => T): Future[Seq[T]]
22-
def prepareAndExecute(sql: String, params: List[Param[_]]): Future[Int]
22+
def prepareAndQuery[T](sql: String, params: List[Param[_]], f: Row => T): Seq[T]
23+
def prepareAndExecute(sql: String, params: List[Param[_]]): Int
2324
}
2425

2526
val mockClient = mock[MockClient]
2627

2728
val client = new PostgresClient {
2829

29-
def prepareAndQuery[T](sql: String, params: Param[_]*)(f: (Row) => T): Future[Seq[T]] =
30-
mockClient.prepareAndQuery(sql, params.toList, f)
30+
def prepareAndQueryToStream[T](sql: String, params: Param[_]*)(f: (Row) => T): AsyncStream[T] =
31+
AsyncStream.fromSeq(mockClient.prepareAndQuery(sql, params.toList, f))
3132

3233
def prepareAndExecute(sql: String, params: Param[_]*): Future[Int] =
33-
mockClient.prepareAndExecute(sql, params.toList)
34+
Future.value(mockClient.prepareAndExecute(sql, params.toList))
3435

3536
def fetch(sql: String): Future[SelectResult] = ???
3637

3738
def execute(sql: String): Future[OK] = ???
3839

3940
def charset: Charset = ???
4041

41-
def select[T](sql: String)(f: (Row) => T): Future[Seq[T]] = ???
42+
def selectToStream[T](sql: String)(f: (Row) => T): AsyncStream[T] = ???
4243

4344
def close(): Future[Unit] = ???
4445

@@ -56,7 +57,7 @@ class QuerySpec extends FreeSpec with Matchers with MockFactory {
5657

5758
def expectQuery[U](expectedQuery: String, expectedParams: Param[_]*)(query: Query[U]) = {
5859
mockClient.prepareAndQuery[U] _ expects (expectedQuery, expectedParams.toList, *) onCall {
59-
(q, p, fn) => Future.value(Seq(fn(row)))
60+
(q, p, fn) => Seq(fn(row))
6061
}
6162
Await.result(query.run(client)).head
6263
}

src/main/scala/com/twitter/finagle/Postgres.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ object Postgres {
178178
private class Dispatcher(transport: Transport[PgRequest, PgResponse], statsReceiver: StatsReceiver)
179179
extends SerialClientDispatcher[PgRequest, PgResponse](transport, statsReceiver) {
180180

181+
override def dispatch(req: PgRequest, p: Promise[PgResponse]): Future[Unit] =
182+
super.dispatch(req, p) before p.flatMap {
183+
case s: AsyncPgResponse =>
184+
// Only release the connection when the state machine has finished processing the events for this request
185+
s.complete
186+
case _ => Future.Done
187+
}
188+
181189
override def apply(
182190
req: PgRequest
183191
): Future[PgResponse] = req match {

src/main/scala/com/twitter/finagle/postgres/PostgresClient.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.twitter.finagle.postgres
22
import java.nio.charset.Charset
33

4+
import com.twitter.concurrent.AsyncStream
45
import com.twitter.finagle.Status
56
import com.twitter.finagle.postgres.messages.SelectResult
67
import com.twitter.finagle.postgres.values.Types
@@ -41,14 +42,16 @@ trait PostgresClient {
4142
/*
4243
* Run a single SELECT query and wrap the results with the provided function.
4344
*/
44-
def select[T](sql: String)
45-
(f: Row => T): Future[Seq[T]]
45+
def select[T](sql: String)(f: Row => T): Future[Seq[T]] =
46+
selectToStream(sql)(f).toSeq
47+
def selectToStream[T](sql: String)(f: Row => T): AsyncStream[T]
4648

4749
/*
4850
* Issue a single, prepared SELECT query and wrap the response rows with the provided function.
4951
*/
50-
def prepareAndQuery[T](sql: String, params: Param[_]*)
51-
(f: Row => T): Future[Seq[T]]
52+
def prepareAndQuery[T](sql: String, params: Param[_]*)(f: Row => T): Future[Seq[T]] =
53+
prepareAndQueryToStream(sql, params: _*)(f).toSeq
54+
def prepareAndQueryToStream[T](sql: String, params: Param[_]*)(f: Row => T): AsyncStream[T]
5255

5356
/*
5457
* Issue a single, prepared arbitrary query without an expected result set, and provide the affected row count

src/main/scala/com/twitter/finagle/postgres/PostgresClientImpl.scala

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import java.nio.charset.{Charset, StandardCharsets}
44
import java.util.concurrent.atomic.AtomicInteger
55

66
import com.twitter.cache.Refresh
7+
import com.twitter.concurrent.AsyncStream
78
import com.twitter.conversions.DurationOps._
89
import com.twitter.finagle.postgres.messages._
910
import com.twitter.finagle.postgres.values._
@@ -51,24 +52,24 @@ class PostgresClientImpl(
5152

5253
val serviceF = factory.apply
5354

54-
val bootstrapTypes = Map(
55-
Types.INT_4 -> ValueDecoder.int4,
56-
Types.TEXT -> ValueDecoder.string
57-
)
55+
def extractTypes(response: PgResponse): Future[Map[Int, PostgresClient.TypeSpecifier]] =
56+
response match {
57+
case SelectResult(fields, rows) =>
58+
val rowValues = ResultSet(fields, charset, rows, PostgresClient.defaultTypes, receiveFunctions).rows
59+
rowValues.map {
60+
row =>
61+
row.get[Int]("oid") -> PostgresClient.TypeSpecifier(
62+
row.get[String]("typreceive"),
63+
row.get[String]("type"),
64+
row.get[Int]("typelem"))
65+
}.toSeq().map(_.toMap)
66+
}
5867

5968
val customTypesResult = for {
6069
service <- serviceF
6170
response <- service.apply(PgRequest(Query(customTypesQuery)))
62-
} yield response match {
63-
case SelectResult(fields, rows) =>
64-
val rowValues = ResultSet(fields, charset, rows, PostgresClient.defaultTypes, receiveFunctions).rows
65-
rowValues.map {
66-
row => row.get[Int]("oid") -> PostgresClient.TypeSpecifier(
67-
row.get[String]("typreceive"),
68-
row.get[String]("type"),
69-
row.get[Int]("typelem"))
70-
}.toMap
71-
}
71+
types <- extractTypes(response)
72+
} yield types
7273

7374
customTypesResult.ensure {
7475
serviceF.foreach(_.close())
@@ -141,25 +142,27 @@ class PostgresClientImpl(
141142
/*
142143
* Run a single SELECT query and wrap the results with the provided function.
143144
*/
144-
override def select[T](sql: String)(f: Row => T): Future[Seq[T]] = for {
145-
types <- typeMap()
146-
result <- fetch(sql)
147-
} yield result match {
148-
case SelectResult(fields, rows) => ResultSet(fields, charset, rows, types, receiveFunctions).rows.map(f)
149-
}
145+
override def selectToStream[T](sql: String)(f: Row => T): AsyncStream[T] =
146+
AsyncStream.fromFuture {
147+
for {
148+
types <- typeMap()
149+
SelectResult(fields, rows) <- fetch(sql)
150+
} yield ResultSet(fields, charset, rows, types, receiveFunctions).rows.map(f)
151+
}.flatten
150152

151153
/*
152154
* Issue a single, prepared SELECT query and wrap the response rows with the provided function.
153155
*/
154-
override def prepareAndQuery[T](sql: String, params: Param[_]*)(f: Row => T): Future[Seq[T]] = {
155-
typeMap().flatMap { _ =>
156-
for {
157-
service <- factory()
158-
statement = new PreparedStatementImpl("", sql, service)
159-
result <- statement.select(params: _*)(f)
160-
} yield result
161-
}
162-
}
156+
override def prepareAndQueryToStream[T](sql: String, params: Param[_]*)(f: Row => T): AsyncStream[T] =
157+
AsyncStream.fromFuture {
158+
typeMap().flatMap { _ =>
159+
for {
160+
service <- factory()
161+
statement = new PreparedStatementImpl("", sql, service)
162+
result <- statement.selectToStream(params: _*)(f)
163+
} yield result
164+
}
165+
}.flatten
163166

164167
/*
165168
* Issue a single, prepared arbitrary query without an expected result set, and provide the affected row count
@@ -292,7 +295,8 @@ class PostgresClientImpl(
292295
exec <- execute()
293296
} yield exec match {
294297
case CommandCompleteResponse(rows) => OK(rows)
295-
case Rows(rows, true) => ResultSet(fields, charset, rows, types, receiveFunctions)
298+
case Rows(rows) =>
299+
ResultSet(fields, charset, rows, types, receiveFunctions)
296300
}
297301
f.transform {
298302
result =>

src/main/scala/com/twitter/finagle/postgres/PreparedStatement.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.twitter.finagle.postgres
22

3+
import com.twitter.concurrent.AsyncStream
34
import com.twitter.finagle.postgres.codec.Errors
45
import com.twitter.util.Future
56

@@ -14,10 +15,12 @@ trait PreparedStatement {
1415
case ResultSet(_) => Future.exception(Errors.client("Update query expected"))
1516
}
1617

17-
def select[T](params: Param[_]*)(f: Row => T): Future[Seq[T]] = fire(params: _*) map {
18+
def selectToStream[T](params: Param[_]*)(f: Row => T): Future[AsyncStream[T]] = fire(params: _*) map {
1819
case ResultSet(rows) => rows.map(f)
19-
case OK(_) => Seq.empty[Row].map(f)
20+
case OK(_) => AsyncStream.empty
2021
}
22+
def select[T](params: Param[_]*)(f: Row => T): Future[Seq[T]] =
23+
selectToStream(params: _*)(f).flatMap(_.toSeq)
2124

2225
def selectFirst[T](params: Param[_]*)(f: Row => T): Future[Option[T]] =
2326
select[T](params:_*)(f) flatMap { rows => Future.value(rows.headOption) }

src/main/scala/com/twitter/finagle/postgres/Responses.scala

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ package com.twitter.finagle.postgres
22

33
import java.nio.charset.Charset
44

5-
import scala.collection.mutable
6-
import scala.collection.mutable.ListBuffer
7-
85
import com.twitter.finagle.postgres.messages.{DataRow, Field}
96
import com.twitter.finagle.postgres.values.ValueDecoder
107
import com.twitter.util.Try
118
import Try._
9+
import com.twitter.concurrent.AsyncStream
1210
import com.twitter.finagle.postgres.PostgresClient.TypeSpecifier
1311
import com.twitter.finagle.postgres.codec.NullValue
1412
import io.netty.buffer.ByteBuf
@@ -157,7 +155,7 @@ sealed trait QueryResponse
157155

158156
case class OK(affectedRows: Int) extends QueryResponse
159157

160-
case class ResultSet(rows: List[Row]) extends QueryResponse
158+
case class ResultSet(rows: AsyncStream[Row]) extends QueryResponse
161159

162160
/*
163161
* Helper object to generate ResultSets for responses with custom types.
@@ -166,7 +164,7 @@ object ResultSet {
166164
def apply(
167165
fields: Array[Field],
168166
charset: Charset,
169-
dataRows: List[DataRow],
167+
dataRows: AsyncStream[DataRow],
170168
types: Map[Int, TypeSpecifier],
171169
receives: PartialFunction[String, ValueDecoder[T] forSome { type T }]
172170
): ResultSet = {

src/main/scala/com/twitter/finagle/postgres/codec/Errors.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.twitter.finagle.postgres.codec
22

33
import com.twitter.finagle.postgres.messages.PgRequest
4+
import com.twitter.finagle.postgres.messages.Error
45

56
/*
67
* An error generated by Postgres.
@@ -40,4 +41,9 @@ object Errors {
4041
hint: Option[String] = None,
4142
position: Option[String] = None) =
4243
ServerError(message, request, severity, sqlState, detail, hint, position)
43-
}
44+
45+
def server(error: Error, request: Option[PgRequest]): ServerError = {
46+
import error._
47+
server(msg.getOrElse("unknown failure"), request, severity, sqlState, detail, hint, position)
48+
}
49+
}

src/main/scala/com/twitter/finagle/postgres/codec/PgCodec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ class HandleErrorsProxy(
4040

4141
def apply(request: PgRequest, service: Service[PgRequest, PgResponse]) = {
4242
service.apply(request).flatMap {
43-
case Error(msg, severity, sqlState, detail, hint, position) =>
44-
Future.exception(Errors.server(msg.getOrElse("unknown failure"), Some(request), severity, sqlState, detail, hint, position))
43+
case e: Error =>
44+
Future.exception(Errors.server(e, Some(request)))
4545
case Terminated =>
4646
Future.exception(new ChannelClosedException())
4747
case r => Future.value(r)

0 commit comments

Comments
 (0)