将Akka流传递到上游服务以进行填充


9

我需要调用上游服务(Azure Blob服务)以将数据推送到OutputStream,然后需要通过akka转过来并将其推送回客户端。没有akka(只有servlet代码),我只需要获取ServletOutputStream并将其传递给azure服务的方法即可。

我可以尝试碰到的最接近的东西,显然这是错误的,是这样的

        Source<ByteString, OutputStream> source = StreamConverters.asOutputStream().mapMaterializedValue(os -> {
            blobClient.download(os);
            return os;
        });

        ResponseEntity resposeEntity = HttpEntities.create(ContentTypes.APPLICATION_OCTET_STREAM, preAuthData.getFileSize(), source);

        sender().tell(new RequestResult(resposeEntity, StatusCodes.OK), self());

我的想法是,我正在调用上游服务,以通过调用blobClient.download(os)获得填充的输出流;

似乎lambda函数被调用并返回,但是随后它失败了,因为没有数据或其他东西。好像我不应该让那个lambda函数来完成这项工作,但是也许返回一些可以完成这项工作的对象?不确定。

如何做到这一点?


的行为是download什么?它是否将数据流式传输os并仅在完成数据写入后才返回?
亚历克

Answers:


2

真正的问题在于,Azure API并非设计用于背压。输出流无法向Azure发信号,表明尚未准备好接收更多数据。换句话说,如果Azure推送数据的速度快于您无法使用数据的速度,则某处必须存在一些难看的缓冲区溢出故障。

接受这个事实,我们可以做的下一个最好的事情是:

  • 使用Source.lazySource仅开始下载数据时,有(亦称源正在运行和数据被请求时)下游需求。
  • download调用放在其他线程中,以便它继续执行而不会阻止源返回。一种方法是使用Future(我不确定Java最佳实践是什么,但是无论哪种方法都可以正常工作)。尽管起初并不重要,但您可能需要选择一个执行上下文,而不是system.dispatcher-它全部取决于是否download阻塞。

如果此Java代码格式错误,我会先道歉-我将Akka与Scala结合使用,因此所有这些都来自于查看Akka Java API和Java语法参考。

ResponseEntity responseEntity = HttpEntities.create(
  ContentTypes.APPLICATION_OCTET_STREAM,
  preAuthData.getFileSize(),

  // Wait until there is downstream demand to intialize the source...
  Source.lazySource(() -> {
    // Pre-materialize the outputstream before the source starts running
    Pair<OutputStream, Source<ByteString, NotUsed>> pair =
      StreamConverters.asOutputStream().preMaterialize(system);

    // Start writing into the download stream in a separate thread
    Futures.future(() -> { blobClient.download(pair.first()); return pair.first(); }, system.getDispatcher());

    // Return the source - it should start running since `lazySource` indicated demand
    return pair.second();
  })
);

sender().tell(new RequestResult(responseEntity, StatusCodes.OK), self());

太棒了 非常感谢。您的示例的一个小修改是:Futures.future(()-> {blobClient.download(pair.first()); return pair.first();},system.getDispatcher());
MeBigFatGuy

@MeBigFatGuy对,谢谢!
亚历克

1

OutputStream这种情况下是“物化值”的Source,并且一旦流运行它只会被创建(或“物化”到运行的流)。由于将其Source交给Akka HTTP,因此运行它已不受您的控制,以后将实际运行您的源代码。

.mapMaterializedValue(matval -> ...)通常用于转换物化值,但是由于它是物化值的一部分而被调用,因此您可以使用它来产生副作用,例如在消息中发送matval,就像您已经弄清楚的那样,不一定有什么不对劲即使看起来很时髦。重要的是要了解,在lambda完成之前,流不会完成其实现并开始运行。这意味着如果download()阻塞而不是分叉另一个线程上的某些工作并立即返回,则会出现问题。

但是,还有另一种解决方案:Source.preMaterialize(),它实现了源,并为您提供了一个Pair实现的值和一个新值Source,可用于消耗已经启动的源:

Pair<OutputStream, Source<ByteString, NotUsed>> pair = 
  StreamConverters.asOutputStream().preMaterialize(system);
OutputStream os = pair.first();
Source<ByteString, NotUsed> source = pair.second();

请注意,您的代码中还有一些其他事情要考虑,最重要的是,如果blobClient.download(os)调用阻塞直到完成,并且您从actor调用,在这种情况下,您必须确保您的actor不要饿死调度程序并停止应用中的其他参与者无法执行(请参阅Akka文档:https : //doc.akka.io/docs/akka/current/typed/dispatchers.html#blocking-needs-careful-management)。


1
感谢您的回复。我不知道这怎么可能?调用blobClient.download(os)时,字节会去哪里(如果我自己调用)?想象有一个TB的数据正在等待写入。在我看来,必须从sender.tell调用中调用blobClient.download调用,这样这基本上是一个类似于IOUtils.copy的操作。.使用preMaterialize,我看不到如何发生?
MeBigFatGuy

OutputStream有一个内部缓冲区,它将开始接受写入,直到该缓冲区填满为止,如果异步下游尚未开始使用元素,则它将阻塞写入线程(这就是为什么我提到处理阻塞很重要的原因)。
johanandren

1
但是,如果我预先进行材质化并获取OutputStream,则正是我的代码正在执行blobClient.download(os);。正确?这意味着它必须先完成才能继续,这是不可能的。
MeBigFatGuy

如果download(os)不能派生线程,则必须处理它正在阻塞的情况,并确保不会停止其他操作。一种方法是派发一个线程来完成工作,另一种方法是首先从actor响应,然后在那里进行阻塞工作,在这种情况下,您必须确保该actor不会饿死其他actor,请参见结尾处的链接。我的答案。
johanandren

在这一点上我只是想让它工作。它甚至无法处理10个字节的文件。
MeBigFatGuy
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.