pulyaevskiy / firebase-functions-interop

Firebase Functions Interop Library for Dart.
BSD 3-Clause "New" or "Revised" License
191 stars 52 forks source link

Timeout issue #43

Closed kostaa closed 5 years ago

kostaa commented 5 years ago

Hi Anatoly,

I have now productionalized the code that is using your library and it all works as expected on small test data samples but once I start putting trough 'proper' data volumes I get a timeout exception on the Flutter side (using the cloud_functions library). The exception is a bit suspicious though as it does not seem to be 'properly formed'. I have been experimenting with data volumes and code changes but cannot get to the bottom of this. I have seen your name in the cloud_functions commit log as well so I though you might have some ideas that will push me in the right direction since you know both libraries.

Bit of context to start with. Part of the Flutter code that uses the Firebase function is doing some data loading and transformation (running on an Android phone I have not tested it on iPhone). The transformation part is quite time consuming and it take some considerable time on older mobiles so I have moved it to the cloud function (server).

The Flutter side (client) extracts first some pieces of data that is required for transformation. This is a list of maps each map containing three items - String identifier, String description and an integer amount. In my tests the number of entries was about 700 to start with but I was getting errors even with as little as 180 entries. The time print outs below are for 700 entries.

This list is pushed to the server by the CloudFunctions.instance.call(...) method and picked up on the other end:

functions['applyRules'] = FirebaseFunctions.https.onCall((dynamic data, CallableContext ctx) => rules(monitoring, data));

Which calls this method:

Map<String, dynamic> rules( Monitoring monitoring, Map<String, dynamic> request) { ... }

I have also tried returning a Future from this method but that did not make any difference.

The data is correctly transported to the server, the server correctly transforms the data but as the server is running the transformation the client gets an exception from the StandardMethodCodec class which I have enhanced a bit to get more info:

  @override
  dynamic decodeEnvelope(ByteData envelope) {
    // First byte is zero in success case, and non-zero otherwise.
    if (envelope.lengthInBytes == 0)
      throw const FormatException('Expected envelope, got nothing');
    final ReadBuffer buffer = ReadBuffer(envelope);

    var xx = buffer.getUint8();
    if (xx == 0)
      return messageCodec.readValue(buffer);

    final dynamic errorCode = messageCodec.readValue(buffer);
    final dynamic errorMessage = messageCodec.readValue(buffer);
    final dynamic errorDetails = messageCodec.readValue(buffer);

    print('************************************************************');
    print('      buffer.getUint8() [${xx}]');
    print('    errorCode is String [${errorCode is String}]');
    print('              errorCode [$errorCode]');
    print('           errorMessage [$errorMessage]');
    print(' errorMessage is String [${errorMessage is String}]');
    print('           errorDetails [$errorDetails]');
    print('    buffer.hasRemaining [${buffer.hasRemaining}]');
    print('************************************************************');

    if (errorCode is String && (errorMessage == null || errorMessage is String) && !buffer.hasRemaining)
      throw PlatformException(code: errorCode, message: errorMessage, details: errorDetails);
    else
      throw const FormatException('Invalid envelope');
  }

This is the client side printout and the exception:

[2018-12-02 11:44:42.857813] Before CloudFunctions
...

************************************************************
       buffer.getUint8() [1]
     errorCode is String [false]
               errorCode [null]
            errorMessage [timeout]
  errorMessage is String [true]
            errorDetails [null]
     buffer.hasRemaining [false]
************************************************************

[2018-12-02 11:44:53.430352] exception

FormatException: Invalid envelope

What is a bit suspicious is that the errorCode is null (although this might be a bug in the underlying Android code).

I also logged the server start and end time and the server finished after the client threw the exception.

2018-12-02 11:44:44.292727489Z  Function execution started

2018-12-02 11:45:18.311019222Z  Function execution took 34020 ms, finished with status code: 200

What is interesting that with the 700 items data set the exception is throw after ~ 11 seconds (based on two observations). Maybe this is a genuine timeout and I need to change a time out property somewhere (as far as I can tell there is none in the Flutter code). But then as I said this happens with much smaller data sets as well which take less time to process.

Regards, Adam

pulyaevskiy commented 5 years ago

Hi Adam,

Thanks for a very thorough explanation! This is indeed an issue with Android implementation of cloud functions onCall trigger as it’s hard coded to use a timeout of 10-15 seconds (in my tests).

See this SO answer: https://stackoverflow.com/questions/51358566/firebase-android-cloud-functions-change-timeout

This is pretty annoying and I’ve experienced it as well in my Flutter project.

The only solution I see is as suggested to rewrite a function as onRequest trigger and use standard http client. This would required to verify user auth yourself though. There is an example in this repo on how to verify user’s id token with Firebase admin sdk, so check it out for more details.

Hope this helps.

kostaa commented 5 years ago

Hmm that is indeed annoying. In my case I have an option to split the data to multiple requests. But it would be good if the the cloud_functions library threw a clean timeout exception to help to detect the correct reason. I will raise an issue there. Many thanks!