基于Publisher的Source不输出元素

时间:2015-08-30 06:01:28

标签: scala akka-stream reactive-streams

我基于ReactiveStreams Publisher制作了Akka Stream的源代码,如下所示:

object FlickrSource {

  val apiKey = Play.current.configuration.getString("flickr.apikey")
  val flickrUserId = Play.current.configuration.getString("flickr.userId")
  val flickrPhotoSearchUrl = s"https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=$apiKey&user_id=$flickrUserId&min_taken_date=%s&max_taken_date=%s&format=json&nojsoncallback=1&page=%s&per_page=500"

  def byDate(date: LocalDate): Source[JsValue, Unit] = {
    Source(new FlickrPhotoSearchPublisher(date))
  }
}

class FlickrPhotoSearchPublisher(date: LocalDate) extends Publisher[JsValue] {

  override def subscribe(subscriber: Subscriber[_ >: JsValue]) {
    try {
      val from = new LocalDate()
      val fromSeconds = from.toDateTimeAtStartOfDay.getMillis
      val toSeconds = from.plusDays(1).toDateTimeAtStartOfDay.getMillis

      def pageGet(page: Int): Unit = {
        val url = flickrPhotoSearchUrl format (fromSeconds, toSeconds, page)
        Logger.debug("Flickr search request: " + url)
        val photosFound = WS.url(url).get().map { response =>
          val json = response.json
          val photosThisPage = (json \ "photos" \ "photo").as[JsArray]
          val numPages = (json \ "photos" \ "pages").as[JsNumber].value.toInt
          Logger.debug(s"pages: $numPages")
          Logger.debug(s"photos this page: ${photosThisPage.value.size}")
          photosThisPage.value.foreach { photo =>
            Logger.debug(s"onNext")
            subscriber.onNext(photo)
          }

          if (numPages > page) {
            Logger.debug("nextPage")
            pageGet(page + 1)
          } else {
            Logger.debug("onComplete")
            subscriber.onComplete()
          }
        }
      }
      pageGet(1)
    } catch {
      case ex: Exception => {
        subscriber.onError(ex)
      }
    }
  }
}

它将向Flickr发出搜索请求,并将结果作为JsValue来源。我尝试将它连接到许多不同的Flows和Sinks,但这将是最基本的设置:

val source: Source[JsValue, Unit] = FlickrSource.byDate(date)
val sink: Sink[JsValue, Future[Unit]] = Sink.foreach(println)
val stream = source.toMat(sink)(Keep.right)
stream.run()

我看到onNext被调用了几次,然后是onComplete。但是,Sink没有收到任何东西。我错过了什么,这不是创建源的有效方法吗?

1 个答案:

答案 0 :(得分:0)

我错误地理解Publisher是一个像Observable这样的简单界面,您可以自己实现。 Akka团队指出,这不是实现发布者的正确方法。事实上,Publisher是一个复杂的类,应该由库而不是最终用户实现。问题中使用的Source.apply(Publisher)方法是与其他Reactive Streams实现的互操作性。

想要实现Source的目的是我想要一个背压源来从Flickr获取搜索结果(每个请求最大化为500)并且我不想做比(或更快)请求更多(或更快)的请求需要下游。这可以通过实现 ActorPublisher 来实现。

<强>更新

这是执行我想要的ActorPublisher:创建一个生成搜索结果的Source,但只生成下游需要的REST调用。我认为还有改进的余地,所以请随时编辑它。

import akka.actor.Props
import akka.stream.actor.ActorPublisher
import akka.stream.actor.ActorPublisherMessage.{Cancel, Request}
import org.joda.time.LocalDate
import play.api.Play.current
import play.api.libs.json.{JsArray, JsNumber, JsValue}
import play.api.libs.ws.WS
import play.api.{Logger, Play}

import scala.concurrent.ExecutionContext.Implicits.global

object FlickrSearchActorPublisher {
  val apiKey = Play.current.configuration.getString("flickr.apikey")
  val flickrUserId = Play.current.configuration.getString("flickr.userId")
  val flickrPhotoSearchUrl = s"https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=$apiKey&user_id=$flickrUserId&min_taken_date=%s&max_taken_date=%s&format=json&nojsoncallback=1&per_page=500&page="

  def byDate(from: LocalDate): Props = {
    val fromSeconds = from.toDateTimeAtStartOfDay.getMillis / 1000
    val toSeconds = from.plusDays(1).toDateTimeAtStartOfDay.getMillis / 1000
    val url = flickrPhotoSearchUrl format (fromSeconds, toSeconds)

    Props(new FlickrSearchActorPublisher(url))
  }
}

class FlickrSearchActorPublisher(url: String) extends ActorPublisher[JsValue] {

  var currentPage = 1
  var numPages = 1
  var photos = Seq[JsValue]()

  def searching: Receive = {
    case Request(count) =>
      Logger.debug(s"Received Request for $count results from Subscriber, ignoring as we are still searching")
    case Cancel =>
      Logger.info("Cancel Message Received, stopping")
      context.stop(self)
    case _ =>
  }

  def accepting: Receive = {
    case Request(count) =>
      Logger.debug(s"Received Request for $count results from Subscriber")
      sendSearchResults()
    case Cancel =>
      Logger.info("Cancel Message Received, stopping")
      context.stop(self)
    case _ =>
  }

  def getNextPageOrStop() {
    if (currentPage > numPages) {
      Logger.debug("No more pages, stopping")
      onCompleteThenStop()
    } else {
      val pageUrl = url + currentPage
      Logger.debug("Flickr search request: " + pageUrl)
      context.become(searching)
      WS.url(pageUrl).get().map { response =>
        val json = response.json
        val photosThisPage = (json \ "photos" \ "photo").as[JsArray]
        numPages = (json \ "photos" \ "pages").as[JsNumber].value.toInt
        Logger.debug(s"page $currentPage of $numPages")
        Logger.debug(s"photos this page: ${photosThisPage.value.size}")
        photos = photosThisPage.value.seq
        if (photos.isEmpty) {
          Logger.debug("No photos found, stopping")
          onCompleteThenStop()
        } else {
          currentPage = currentPage + 1
          sendSearchResults()
          context.become(accepting)
        }
      }
    }
  }

  def sendSearchResults() {
    if (photos.isEmpty) {
      getNextPageOrStop()
    } else {
      while(isActive && totalDemand > 0) {
        onNext(photos.head)
        photos = photos.tail
        if (photos.isEmpty) {
          getNextPageOrStop()
        }
      }
    }
  }

  getNextPageOrStop()
  val receive = searching
}