停止和启动后,双向gRPC流有时会停止处理响应

时间:2018-02-23 08:49:33

标签: ios swift grpc

简而言之

我们有一个移动应用程序,可通过各种双向流向服务器传输大量数据。有时需要关闭流(例如,当应用程序背景化时)。然后根据需要重新打开它们。有时,当发生这种情况时,出现问题:

  • 据我所知,该流已在设备的一侧启动并运行(GRPCProtocall和GRXWriter的状态均已启动或暂停)
  • 设备在流上发送数据(服务器接收数据)
  • 服务器似乎将数据发送回设备(服务器的Stream.Send调用返回成功)
  • 在设备上,流上收到的数据的结果处理程序永远不会被调用

更多细节

我们的代码在下面进行了大量简化,但是这应该提供足够的细节来表明我们正在做什么。双向流由Switch类管理:

class Switch {
    /** The protocall over which we send and receive data */
    var protocall: GRPCProtoCall?

    /** The writer object that writes data to the protocall. */
    var writer: GRXBufferedPipe?

    /** A static GRPCProtoService as per the .proto */
    static let service = APPDataService(host: Settings.grpcHost)

    /** A response handler. APPData is the datatype defined by the .proto. */
    func rpcResponse(done: Bool, response: APPData?, error: Error?) {
        NSLog("Response received")
        // Handle response...
    }

    func start() {
        // Create a (new) instance of the writer
        // (A writer cannot be used on multiple protocalls)
        self.writer = GRXBufferedPipe()

        // Setup the protocall
        self.protocall = Switch.service.rpcToStream(withRequestWriter: self.writer!, eventHandler: self.rpcRespose(done:response:error:))

        // Start the stream
        self.protocall.start()
    }

    func stop() {
        // Stop the writer if it is started.
        if self.writer.state == .started || self.writer.state == .paused {
            self.writer.finishWithError(nil)
        }

        // Stop the proto call if it is started
        if self.protocall?.state == .started || self.protocall?.state == .paused {
            protocall?.cancel()
        }
        self.protocall = nil
    }

    private var needsRestart: Bool {
        if let protocall = self.protocall {
            if protocall.state == .notStarted || protocall.state == .finished {
                // protocall exists, but isn't running.
                return true
            } else if writer.state == .notStarted || writer.state == .finished {
                // writer isn't running
                return true
            } else {
                // protocall and writer are running
                return false
            }
        } else {
            // protocall doesn't exist.
            return true
        }
    }

    func restartIfNeeded() {
        guard self.needsRestart else { return }
        self.stop()
        self.start()
    }

    func write(data: APPData) {
        self.writer.writeValue(data)
    }
}

就像我说的那样,大大简化了,但它显示了我们如何启动,停止和重启流,以及我们如何检查流是否健康。

当应用程序背景化时,我们会调用stop()。当它被预先考虑并且我们再次需要流时,我们会调用start()。我们会定期致电restartIfNeeded(),例如。当使用流的屏幕进入视图时。

正如我上面提到的,偶尔发生的是当服务器将数据写入流时,我们的响应处理程序(rpcResponse)停止被调用。流似乎是健康的(服务器接收我们写入的数据,protocall.state既不是.notStarted也不是.finished)。但是甚至没有执行响应处理程序第一行的日志。

第一个问题:我们是正确管理流,还是我们停止和重新启动容易出错的流的方式?如果是这样,做这样的事情的正确方法是什么?

第二个问题:我们如何调试这个?我们可以想到的一切,我们可以查询状态告诉我们流已经启动并运行,但感觉就像objc gRPC库保留了很多隐藏在我们身上的机制。有没有办法看看服务器的响应是否到达我们,但是无法触发我们的响应处理程序?

第三个问题:根据上面的代码,我们使用库提供的GRXBufferedPipe。它的文档建议不要在生产中使用它,因为它没有推回机制。根据我们的理解,编写器仅用于以同步,一次一个的方式向gRPC核心提供数据,并且由于服务器从我们这里接收数据很好,我们不认为这是一个问题。我们错了吗?作者是否还参与将从服务器接收的数据提供给我们的响应处理程序?即如果作者因超载而崩溃,那么从流中读取数据是否有问题,而不是写入?

更新:在问这个问题一年多后,我们终于在服务器端代码中发现了一个死锁错误,导致客户端出现这种情况。流似乎挂起,因为客户端发送的通信没有由服务器处理,反之亦然,但流实际上还活着。接受的答案为如何管理这些双向流提供了很好的建议,我认为这仍然很有价值(它帮助了我们很多!)。但问题实际上是由于编程错误。

此外,对于遇到此类问题的任何人,可能值得调查一下,当iOS更改其网络时,您是否正在体验this known issue通道被静默删除的情况。 This readme提供了使用Apple的CFStream API而不是TCP套接字作为该问题的可能修复程序的说明。

1 个答案:

答案 0 :(得分:2)

  

第一个问题:我们是正确管理流,还是我们停止和重新启动容易出错的流的方式?如果是这样,做这样的事情的正确方法是什么?

通过查看代码我可以看出,start()函数似乎是正确的。在stop()函数中,您无需致电cancel()的{​​{1}};呼叫将使用之前的self.protocall完成。

self.writer.finishWithError(nil)是一个有点凌乱的地方。首先,您不应该自己轮询/设置needsrestart()的状态。该状态本身就会改变。其次,设置这些状态不会关闭您的流。它只会暂停一个作家,如果app在后台,暂停一个作家就像一个无操作。如果要关闭流,则应使用protocall来终止此呼叫,并在以后需要时启动新呼叫。

  

第二个问题:我们如何调试这个?

一种方法是打开gRPC日志(GRPC_TRACE和GRPC_VERBOSITY)。另一种方法是在here设置断点,其中gRPC objc库从服务器接收gRPC消息。

  

第三个问题:作者是否也参与将从服务器收到的数据提供给我们的响应处理程序?

没有。如果您创建缓冲管道并将其作为呼叫请求提供,则它仅提供要发送到服务器的数据。接收路径由另一个编写器(实际上是您的finishWithError对象)处理。

我不知道不鼓励在生产中使用protocall的地方。此实用程序的已知缺点是,如果您暂停编写器但仍使用GRXBufferedPipe向其写入数据,则最终会缓冲大量数据而无法刷新它们,这可能会导致内存问题。