Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: Running Bolt apps on Function-as-a-Service #361

Closed
4 of 9 tasks
seratch opened this issue Dec 25, 2019 · 6 comments
Closed
4 of 9 tasks

Discussion: Running Bolt apps on Function-as-a-Service #361

seratch opened this issue Dec 25, 2019 · 6 comments
Assignees
Labels
discussion M-T: An issue where more input is needed to reach a decision
Milestone

Comments

@seratch
Copy link
Member

seratch commented Dec 25, 2019

Description

Disclaimer

Creating this issue doesn't mean the Bolt team is going to provide a proper way to support FaaS (e.g., AWS Lambda, Google Cloud Functions, Cloud Functions for Firebase, etc.) in the short run.

I wanted to create a place to discuss any future possibilities to support FaaS in some ways.

The challenge we have with FaaS

One limitation you should know when you run Bolt apps on FaaS is that any asynchronous operations can be silently terminated once its internet-facing function responds to an incoming HTTP request from Slack.

Examples of "asynchronous operations" here are say, respond, app.client calls, and whatever internally starts Promise operations separately from ack() completion.

Let's say you want to create an AWS Lambda function that is free from Slack's 3-second timeouts and the limitation I mentioned above. In this case, the following code is a solution.

const { App, ExpressReceiver } = require('@slack/bolt');
import AWS = require('aws-sdk');

const receiver = new ExpressReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET
});
const app = new App({
  receiver,
  token: process.env.SLACK_BOT_TOKEN,
});

app.command("/hello", ({ body, ack }) => {
  if (isLocalDevelopment()) {
    ack();
    mainLogic(body);
  } else {
    const lambda = new AWS.Lambda();
    const params: AWS.Lambda.InvocationRequest = {
      InvocationType: 'Event', // async invocation
      FunctionName: functionName,
      Payload: JSON.stringify(body)
    };
    const lambdaInvocation = await lambda.invoke(params).promise();
    // check the lambdaInvocation here
    ack();
  }
});

function mainLogic(body) {
  // do something it may take a long time
}

// frontend: internet-facing function handling requests from Slack
const awsServerlessExpress = require('aws-serverless-express');
const server = awsServerlessExpress.createServer(receiver.app);
module.exports.frontend = (event, context) => {
  awsServerlessExpress.proxy(server, event, context);
}

// backend: internal function for anything apart from acknowledging requests from Slack
module.exports.backend = async function (event, _context) {
  // if you reuse this function for other patterns, need to dispatch the events
  await mainLogic(event);
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'done' }),
  };
};

One possible idea I came up with

Let me refer to my comment in another issue: #353 (comment)

One feasible idea is creating a FaaS support package as a 3rd-party one. That means the package won't be managed by @slackapi. The authors of such packages can be anyone. If someone starts such projects, I'll be more than happy to support them.

As a first step, I'll publish my prototype implementation in my own repository and will start a new GitHub issue in this repository. I hope it will be a good starting point for people interested in FaaS support. Even though my prototype is still in very alpha quality, checking it could be helpful for the folks that may start new projects.

If Bolt changes its APIs and internals, it may be feasible to have a 3rd-party generalized npm package that offers proper FaaS supports for Bolt.

Here is my prototype demonstrating it: https://github.com/seratch/bolt-aws-lambda-proof-of-concept
It doesn't support all the features yet but it just works for supported cases.

Here is a simple example code (just pasted from the repository's README).

const app = new TwoPhaseApp({
  token: process.env.SLACK_BOT_TOKEN,
  // this receiver tries to get AWS credentials from env variables by default
  receiver: new AwsLambdaReceiver({
    signingSecret: process.env.SLACK_SIGNING_SECRET
  })
});

app.command('/lambda')
  .ack(({ ack }) => {
    // phase1 function: compatible with current listener function
    ack('ack response');
  })
  .then(async ({ body, say }) => {
    // phase2 function: this one is invoked as another lambda function
    return say('How are you?').then(() => say("I'm good!"));
  });

Now developers don't need to directly use AWS Lambda SDK. AwsLambdaReceiver does everything for you: https://github.com/seratch/bolt-aws-lambda-proof-of-concept/blob/7b72a5e416977036e98c4bfbf40ee0567910766c/src/added/AwsLambdaReceiver.ts#L174-L189

Apart from the things specific to AWS, this approach is applicable to any others (not only FaaS).

In this design, the two phases are:

  • Phase 1: internet-facing handler - responsible for request acknowledgment and synchronous things (e.g., signature verification, dispatching requests, payload validation, and responding a message as part of HTTP responses)
  • Phase 2: internal function - can do anything with the relayed request body asynchronously / use respond to send a message to a user asynchronously

Phase 2 function is supposed to return a single Promise as its result. The design makes easier to write code in idiomatic ways (like using then/catch and/or Promise.all) for working with Promises.

Next steps

If someone is interested in starting with my PoC prototype to build a 3rd-party library, I'm happy to donate my code (it's under the MIT license) and will be more than happy to contribute to it as an individual.

To realize such 3rd parties, a few changes on the Bolt side are still needed. @aoberoi shared a related proposal at #353. Join the conversation to share your thoughts and/or feedback.

What type of issue is this? (place an x in one of the [ ])

  • bug
  • enhancement (feature request)
  • question
  • documentation related
  • testing related
  • discussion

Requirements (place an x in each of the [ ])

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.
@seratch seratch added the discussion M-T: An issue where more input is needed to reach a decision label Dec 25, 2019
@seratch seratch self-assigned this Dec 25, 2019
@threesquared
Copy link

If it helps at all I had some success running Bolt on Lambda in this app by using the express receiver and aws-serverless-express. It seems to work as intended but has not been extensively tested.

@seratch
Copy link
Member Author

seratch commented Jan 10, 2020

@threesquared Thanks for your comment. Yes, for simple apps, it works mostly. If a Bolt app has time-consuming Promises, those operations may be unexpectedly terminated. The current Bolt implementation doesn't offer a proper way to deal with the problem.

@seratch seratch added this to the V2 milestone Feb 4, 2020
@seratch
Copy link
Member Author

seratch commented Feb 4, 2020

With Bolt v2, it'll be much safer to build a Bolt app with FaaS. The say, respond, and whatever that generates Promises in code won't be terminated as long as a developer handles them properly as below.

app.command("/echo", async ({ say, ack }) => {
  say("something").then(() => ack());
});

function doSomething(): Promise<string> { ... }
app.command("/echo", async ({ ack }) => {
  doSomething().then(() => ack());
});

In addition, I'll come up with the initial version of the TwoPhaseApp as an experimental (and probably as my personal project) after shipping Bolt 2.0.

@seratch
Copy link
Member Author

seratch commented Feb 5, 2020

If you're curious about the improvements for app.event listeners in v2, check the issue #395

@seratch seratch modified the milestones: v2.0, v2.1 Mar 26, 2020
@aoberoi aoberoi modified the milestones: v2.1, v2.0 Mar 27, 2020
@aoberoi
Copy link
Contributor

aoberoi commented Mar 27, 2020

Seeing as we have a solution that we've converged on, I think we can safely close this issue. I think the discussion portion of this problem is done. What do you think @seratch?

@seratch
Copy link
Member Author

seratch commented Mar 27, 2020

You're right. We can safely close this ticket as we've merged some effective changes for listeners. Regarding #395 (more specific one for Events API), I'd like to hold off closing as it's still in progress (we haven't merged @stevengill's PR yet). But we can close #361 now. Thanks for taking care of this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion M-T: An issue where more input is needed to reach a decision
Projects
None yet
Development

No branches or pull requests

3 participants