2

I want represent external process execution as Observable[String], where String - line from process output. Here example what I do, it's worked:

import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import monix.reactive.Observable

object TestSo {

  def main(args: Array[String]): Unit = {

    val lineStream = scala.sys.process.Process("python3 test.py").lineStream

    val lineStreamO: Observable[String] = Observable.fromIterator(Task(lineStream.iterator))
      .doOnNext(l => Task(println(l))) //logging
      .guarantee(Task(println("clean resources")))

    println(lineStreamO.toListL.runSyncUnsafe())
  }

}

You can see, that process emits new line every second. But it does not matter. Just provide full example, test.py:

from time import sleep
print(0, flush=True)
sleep(1)
print(1, flush=True)
sleep(1)
print(2, flush=True)
sleep(1)
print(3, flush=True)
sleep(1)
print(4, flush=True)

Output:

0
1
2
3
4
5
clean resources
List(0, 1, 2, 3, 4, 5)

Problem:

I want to have timeout - if process freezes (for example sleep 100000) process should be killed after timeout. Also if process compelted or fail, some resources should be cleaned (guarantee in example). NonZero exit code should represent failure.

How to implement process execution as Observable[String] with propper error handling? rx-java solutions are welcome.

zella
  • 4,645
  • 6
  • 35
  • 60

2 Answers2

2

The need of a timeout will force you to re-write a major part of the lineStream logic. On the other hand with such a re-write you can avoid an intermediate Iterator and directly push lines into a Subject. For the timeout logic you can use Monix timeoutOnSlowUpstream method but you still have to handle the timeout error and close the started process.

Also there is a choice on what to do with long output and several subscribers. In this code I decided to use a limited buffer of replayLimited. Depending on your needs you might choose some different strategy. Here is a sketch of a solution:

object ProcessHelper {

  import scala.sys.process.{Process, BasicIO}
  import scala.concurrent.duration.FiniteDuration
  import monix.eval.Task
  import monix.execution.Scheduler
  import monix.reactive.subjects.ConcurrentSubject
  import monix.reactive.Observable

  private class FinishedFlagWrapper(var finished: Boolean = false)

  def buildProcessLinesObservable(cmd: String, timeout: FiniteDuration, bufferLines: Int = 100)(implicit scheduler: Scheduler): Observable[String] = {
    // works both as a holder for a mutable boolean var and as a synchronization lock
    // that is required to preserve semantics of a Subject, particularly
    // that onNext is never called after onError or onComplete
    val finished = new FinishedFlagWrapper()

    // whether you want here replayLimited or some other logic depends on your needs
    val subj = ConcurrentSubject.replayLimited[String](bufferLines)

    val proc = Process(cmd).run(BasicIO(withIn = false,
      line => finished.synchronized {
        if (!finished.finished)
          subj.onNext(line)
      }, None))

    // unfortunately we have to block a whole thread just to wait for the exit code
    val exitThread = new Thread(() => {
      try {
        val exitCode = proc.exitValue()
        finished.synchronized {
          if (!finished.finished) {
            finished.finished = true
            if (exitCode != 0) {
              subj.onError(new RuntimeException(s"Process '$cmd' has exited with $exitCode."))
            }
            else {
              subj.onComplete()
            }
          }
        }
      }
      catch {
        // ignore when this is a result of our timeout
        case e: InterruptedException => if(!finished.finished) e.printStackTrace()
      }
    }, "Process-exit-wait")
    exitThread.start()

    subj.timeoutOnSlowUpstream(timeout)
      .guarantee(Task(finished.synchronized {
        if (!finished.finished) {
          finished.finished = true
          proc.destroy()
          exitThread.interrupt()
        }
      }))
  }
}

Usage example would be something like:

def test(): Unit = {
  import monix.execution.Ack._
  import monix.reactive._
  import scala.concurrent._
  import scala.concurrent.duration._
  import monix.execution.Scheduler.Implicits.global


  val linesO = ProcessHelper.buildProcessLinesObservable("python3 test.py", 5 seconds, 2) // buffer is reduced to just 2 lines just for this example 

  linesO.subscribe(new Observer[String] {
    override def onNext(s: String): Future[Ack] = {
      println(s"Received '$s'")
      Future.successful(Continue)
    }

    override def onError(ex: Throwable): Unit = println(s"Error '$ex'")

    override def onComplete(): Unit = println("Complete")
  })

  try {
    println(linesO.toListL.runSyncUnsafe())
    println(linesO.toListL.runSyncUnsafe()) // second run will show only last 2 values because of the reduced buffer size
    println("Finish success")
  }
  catch {
    case e: Throwable => println("Failed with " + e)
  }
}
SergGr
  • 23,570
  • 2
  • 30
  • 51
  • In case of `while(True): sleep 1` and timeout bigger than 1, process never ends. Any hints how to implement "summary" timeout? – zella Jan 17 '19 at 16:35
  • @zella, sorry, I'm not sure I get you. For me `while(True): sleep(1)` works as expected i.e. stops by timeout. Did you mean `while(True): print(something) sleep(1)`? Well in this case I see a fundamental conflict in your requirements. Still if this is what you want for some reason, you can just create an `Observable.evalDelayed`, subscribe to it and do the same stuff I do when `exitCode != 0` i.e. assign `finished.finished = true` and do `subj.onError` with some exception. – SergGr Jan 17 '19 at 16:45
  • `subj.takeUntil(Observable.fromTask(Task.sleep(timeout)))` worked for me. Thanks, I have learned a lot of new things today. – zella Jan 17 '19 at 16:47
  • @zella, note that there is a subtle but potentially important difference between `takeUntil` and what I suggested. Particularly `takeUntil` will end with successful `onComplete` while my approach will generate `onError` which is IMHO a more appropriate for a timeout. Still `onComplete` might be what you want in your particular use case. – SergGr Jan 17 '19 at 16:52
  • 1
    Seems like `subj.takeUntil(Observable.evalDelayed(timeout, throw new TimeoutException))` worked. – zella Jan 17 '19 at 16:59
  • @zella, yes, that will probably work. `takeUntil` passes the same exception to the main `Observable` – SergGr Jan 17 '19 at 17:01
0

I implemened process execution as reactive rxjava2 Observable in small library, that wraps NuProcess in reactive way. For example:

PreparedStreams streams = builder.asStdInOut();

Single<NuProcess> started = streams.started();
Single<Exit> done = streams.waitDone();
Observable<byte[]> stdout = streams.stdOut();
Observer<byte[]> stdin = streams.stdIn();

done.subscribe();
zella
  • 4,645
  • 6
  • 35
  • 60