0

I'm using rxscala and found a very subtle problem, and my code is simplied to the following:

import rx.lang.scala.Observable
import rx.lang.scala.subjects.PublishSubject

object SubtleBug extends App {

  case class Projects(projects: List[Project] = Nil)
  case class Project(name: String, docs: List[Doc] = Nil)
  case class Doc(path: String, baseContent: String)

  sealed trait ServerEvent
  case class ProjectNames(projects: Seq[String]) extends ServerEvent
  case class NewDocument(projectName: String, path: String, version: Int, content: String) extends ServerEvent

  val receivedEvents = PublishSubject[ServerEvent]

  new Thread(new Runnable {
    override def run(): Unit = {
      val events = Seq(
        new ProjectNames(Seq("p1", "p2")),
        NewDocument("p1", "/aaa", 1, "my-content"),
        NewDocument("p1", "/bbb", 1, "my-content")
      )
      events.foreach { event =>
        receivedEvents.onNext(event)
        Thread.sleep(200)
      }
    }
  }).start()

  lazy val projects: Observable[Option[Projects]] = receivedEvents.scan(Option.empty[Projects]) {
    case (_, ProjectNames(names)) => {
      Some(Projects(names.map(name => Project(name)).toList))
    }
    case (Some(ps), NewDocument(projectName, docPath, version, content)) => {
      val doc = Doc(docPath, content)
      val newPs = ps.copy(projects = ps.projects.map {
        case project if project.name == projectName => project.copy(docs = project.docs ::: List(doc))
        case p => p
      })
      Some(newPs)
    }
    case _ => None
  }.collect({
    case Some(p) => Some(p)
  })

  projects.foreach(ps => {
    println("### 111: " + ps.map(_.projects))
    projects.foreach(x => println("### 222: " + ps.map(_.projects))) // !!!(2)
  })

  Thread.sleep(2000)

}

The key point is the !!!(2) line, which is inside the projects itself.

It prints the following:

### 111: Some(List(Project(p1,List()), Project(p2,List())))
### 111: Some(List(Project(p1,List(Doc(/aaa,my-content))), Project(p2,List())))
### 111: Some(List(Project(p1,List(Doc(/aaa,my-content), Doc(/bbb,my-content))), Project(p2,List())))

The problem is there is no ### 222: lines!

But if I change the collect part, and adding the None case:

.collect({
  case Some(p) => Some(p)
  case None => None // added case
})

It will print the ### 222: lines as I expected:

### 111: None
### 111: Some(List(Project(p1,List()), Project(p2,List())))
### 111: Some(List(Project(p1,List(Doc(/aaa,my-content))), Project(p2,List())))
### 222: None
### 222: None
### 222: Some(List(Project(p1,List()), Project(p2,List())))
### 222: Some(List(Project(p1,List()), Project(p2,List())))
### 111: Some(List(Project(p1,List(Doc(/aaa,my-content), Doc(/bbb,my-content))), Project(p2,List())))
### 222: None
### 222: Some(List(Project(p1,List()), Project(p2,List())))
### 222: Some(List(Project(p1,List(Doc(/aaa,my-content))), Project(p2,List())))
### 222: Some(List(Project(p1,List(Doc(/aaa,my-content))), Project(p2,List())))

I can't understand why.

PS: You can clone the code here: https://github.com/freewind/rxscala-test/blob/master/src/main/scala/myrx/SubtleBug.scala

Freewind
  • 193,756
  • 157
  • 432
  • 708

1 Answers1

0

There is a race condition in your codes. PublishSubject will drop any events before you subscribe it. So if receivedEvents.onNext(event) runs before projects.foreach, event will be dropped. And in scan, if the first event is dropped, the pattern match won't work any more.

You can use ReplaySubject to fix it.

zsxwing
  • 20,270
  • 4
  • 37
  • 59
  • That's strange, I just tried the code and it will output the `### 111: Some(...` lines as in my question. You might miss the method [receivedEvents.scan](http://reactivex.io/documentation/operators/scan.html), which is like `foldLeft`, so I can passing a `Option.empty[Projects]` as match with it – Freewind Nov 21 '15 at 00:17