xp-forge / lambda

AWS Lambda for the XP Framework
3 stars 0 forks source link

Implement streaming lambda responses #23

Closed thekid closed 1 year ago

thekid commented 1 year ago

This pull request implements feature request #22, AWS Lambda response streaming as announced by AWS in April 2023. Its documentation states:

Currently, Lambda supports response streaming only on Node.js 14.x, Node.js 16.x, and Node.js 18.x managed runtimes. [...] You can stream responses through Lambda Function URLs, the AWS SDK, or using the Lambda InvokeWithResponseStream API.

To use the stream, return a function(var, Stream, Context) from the handler's target() method instead of a function(var, Context).

Example code

use com\amazon\aws\lambda\Handler;

class Streamed extends Handler {

  public function target(): callable {
    return function($event, $stream, $context) {
      $stream->use('text/plain');
      $stream->write("[".date('r')."] Hello world...\n");

      sleep(1);

      $stream->write("[".date('r')."] ...from Lambda\n");
      $stream->end();
    };
  }
}

The transmit() method can be used to stream from input streams, e.g. from a file, or a socket. Response stream payloads have a soft limit of 20 MB as compared to the 6 MB limit for buffered responses.

use com\amazon\aws\lambda\Handler;
use io\File;
use util\MimeType;

class Streamed extends Handler {

  public function target(): callable {
    return function($event, $stream, $context) {
      $file= new File(/* ... */);
      $stream->transmit($file, MimeType::getByFileName($file->filename));
    };
  }
}

Stream API

public interface com.amazon.aws.lambda.Stream extends io.streams.OutputStream, lang.Closeable {
  public abstract function transmit(io.Channel|io.streams.InputStream $source, string $mimeType): void
  public abstract function use(string $mimeType): void
  public abstract function write(string $bytes): void
  public abstract function end(): void
  public abstract function flush(): void
  public abstract function close(): var
}

Deployment

service: xp-streaming

provider:
  name: aws
  region: eu-central-1

functions:
  func:
    handler: Streamed
    runtime: provided.al2
    description: 'XP Streaming'
    url:
      invokeMode: RESPONSE_STREAM
    package:
      individually: true
      artifact: function.zip
    layers:
      - arn:aws:lambda:eu-central-1:123456789012:layer:lambda-xp-runtime2:1

Invocation

xp-lambda-stream

TODO

thekid commented 1 year ago

For being able to use this with web apps

https://docs.aws.amazon.com/lambda/latest/dg/response-streaming-tutorial.html#response-streaming-tutorial-create-function-cfn shows how HTTP headers can be returned:

exports.handler = awslambda.streamifyResponse(
  async (event, responseStream, _context) => {
    // Metadata is a JSON serializable JS object. Its shape is not defined here.
    const metadata = {
      statusCode: 200,
      headers: {
        "Content-Type": "application/json",
        "CustomHeader": "outerspace"
      }
    };

    // Assign to the responseStream parameter to prevent accidental reuse of the non-wrapped stream.
    responseStream = awslambda.HttpResponseStream.from(responseStream, metadata);

    responseStream.write("...");
    responseStream.end();
  }
);

This seems to be implemented by using a special content type, packing the meta data as JSON and delimiting it from the rest of the payload with 8 NUL bytes.

Content-Type: application/vnd.awslambda.http-integration-response

{"statusCode":200,"headers:{"Content-Type":"application/json","CustomHeader":"outerspace"}}
\0\0\0\0\0\0\0\0
...

See https://gist.github.com/serg06/0653a4c690a7f5854f8038e3ebe62a64

POC

use com\amazon\aws\lambda\Handler;
use text\json\{Json, StreamOutput};

class Web extends Handler {

  public function target(): callable {
    return function($event, $stream, $context) {
      $stream->use('application/vnd.awslambda.http-integration-response');

      $stream->write('{"statusCode":200,"headers":{"Content-Type":"application/json"}}');
      $stream->write("\x00\x00\x00\x00\x00\x00\x00\x00");

      Json::write(['event' => $event], new StreamOutput($stream));
    };
  }
}
thekid commented 1 year ago

✅ Verified xp-forge/lambda-ws is fully compatible with these changes.

However, changing the invoke mode on existing lambdas built with this library will yield incorrect results, displaying the meta information instead of using it for the request:

Mode BUFFERED: Correct behavior

Mode RESPONSE_STREAM: Incorrect behavior

This is nothing special to the XP Framework, it will happen with any NodeJS lambda, too!

As there seems to be no way to detect the invoke mode at runtime except for using the AWS lambda management APIs (which would be quite slow!), and because the other way around (a function using streaming on the inside but configured to use BUFFERED mode) will not breaks, our path forward should most probably be the following:

thekid commented 1 year ago

Released in https://github.com/xp-forge/lambda/releases/tag/v5.0.0