knative/func

Knative functions are not stateless

kpatel71716 opened this issue ยท 12 comments

Expected Behavior

Knative functions should be stateless in execution. If I call func invoke multiple times, each invocation should return the same result.

Actual Behavior

Multiple Knative function invocations error out on subsequent requests

Steps to Reproduce the Problem

  1. Create a Knative function (func create -l typescript example). Note, this is all only tested locally on my machine
  2. Install the firebase-admin dependency (firebase-admin)
  3. Create a firebase app
  4. Use the following code in the generated index.js file:
import { Context, StructuredReturn } from 'faas-js-runtime';

const handle = async (context: Context, body: string): Promise<StructuredReturn> => {
  const { initializeApp } = require("firebase-admin/app");
  const firebaseConfig = {
    apiKey: "<API_KEY>",
    authDomain: "<AUTH_DOMAIN>",
    projectId: "<PROJECT_ID>",
    storageBucket: "<STORAGE_BUCKET>",
    messagingSenderId: "<SENDER_ID>",
    appId: "<APP_ID>"
  };

  // Initialize Firebase
  const app = await initializeApp(firebaseConfig);
  
  context.log.info(`
-----------------------------------------------------------
Headers:
${JSON.stringify(context.headers)}

Query:
${JSON.stringify(context.query)}

Body:
${JSON.stringify(body)}
-----------------------------------------------------------
`);
  return {
    body: app,
    headers: {
      'content-type': 'application/json'
    }
  };
};

export { handle };

  1. Run the function via func invoke. Notice that the function succeeds
  2. Run the function again and notice that the invocation errors with Called reply with an invalid status code: app/duplicate-app
  3. Restart the container and repeat step 5 and notice that the function does not error

Additional Info

Thank you all for all of the hard work that has gone into this product! Hopefully I am just missing a small thing. If Knative functions were stateless, I would expect multiple invocations to succeed without error. It seems that initializeApp writes to a global variable that is persisted throughout the life of the container. Do the failing subsequent func invoke commands imply that the above function is not stateless?

lance commented

@kpatel71716 thanks for the kind words about the project! Not all runtimes support this yet, but for Node.js and TypeScript, you can export an object that conforms to the Function interface defined here: https://github.com/nodeshift/faas-js-runtime/blob/9dc620d21c7804b62af211316b5cf382866e94ea/lib/types.d.ts#L10-L31

This will allow you to call initializeApp() in the init() function of your exported object. This function will only be called once for the lifetime of the container runtime.

Hope this helps.

@kpatel71716 has @lance answered you question? I think that as suggested you are supposed to put init code in init() function and then export it along with handle function.

Thanks for the quick reply @lance! That information is certainly helpful. However, if I would like every request to call initializeApp() or, more likely, some other function that exhibits identical behavior to the above example, is there a way to prevent data from being written globally in a request? I am not sure if additional isolation is possible in a container-based setup

lance commented

is there a way to prevent data from being written globally in a request?

No - as you suggest, a container based deployment model implies that any single instance of a deployed function may be invoked multiple times. State management should be external to the function, except for initialization and shutdown behavior as outlined in the faas-js-runtime module.

However, if I would like every request to call initializeApp()

I believe that for some initialization logic it is better to be called once at start up. As an example we can take initialization of DB connection pool. It does not make sense to do such a thing per request, it is better to setup pool which is then used by incoming request.

is there a way to prevent data from being written globally in a request?

I am no JS expert but probably only reliable way would be running separate JS VM per request which would be terrible performance-wise.

Even if you look for examples for AWS Lambda you will see they are not initializing Firebase in handler. They are initializing it at top level of js source.

If you insist on initializing Firebase in handle you should guard it

const { initializeApp } = require("firebase-admin/app");
const firebaseConfig = {
  apiKey: "<API_KEY>",
  authDomain: "<AUTH_DOMAIN>",
  projectId: "<PROJECT_ID>",
  storageBucket: "<STORAGE_BUCKET>",
  messagingSenderId: "<SENDER_ID>",
  appId: "<APP_ID>"
};
let app = null;

const handle = async (context: Context, body: string): Promise<StructuredReturn> => {
  // Initialize Firebase
  if (app == null) {
    app = await initializeApp(firebaseConfig)
  }
}

Maybe we could try re-bind global context. Still I am not sure you are supposed to call initializeApp multiple times.

Maybe we are even binding global context https://github.com/nodeshift/faas-js-runtime/pull/118/files but that is not sufficient.

Thanks @lance and @matejvasek, the information is very helpful! However, my question is broader than just this firebase example and I should clarify my goal.

My goal is to create a service where clients can define some javascript to execute, similar to Lambda conceptually. I am exploring spinning up a container for each client using their pre-defined javascript. Something like:

import { Context, StructuredReturn } from 'faas-js-runtime';

const handle = async (context: Context, body: string): Promise<StructuredReturn> => {
  return userDefinedFunction(context, body);
};

export { handle };

where userDefinedFunction(context, body);would execute some js that the client has defined. e.g. the user would just see/save something like the following

exports = async function(arg){
  return "Hello World!"
}

The difficulty with this approach is that clients wouldn't have the option to define the init function since those details would be obfuscated. I agree that initializeApp() shouldn't be called in this way but I imagine there are other examples that a client could unknowingly run into, especially when using dependencies. Is there a path forward related to the global context as @matejvasek mentioned? Or would this be a limitation of creating a service with this type of solution as @lance suggested?

lance commented

My goal is to create a service where clients can define some javascript to execute, similar to Lambda conceptually.

@kpatel71716 it sounds almost as though you are trying to build a faas using Knative Functions.

where userDefinedFunction(context, body);would execute some js that the client has defined.

This is essentially the behavior of the faas-js-runtime module. It's not clear to me what the benefit of layering the additional behavior you are describing on top of Knative Functions would be.

Given the architecture of this project, I don't think your goal will be easy to achieve or be sufficiently performant.

Thanks @lance, I am experimenting with different solutions in trying to build a faas. I will explore the faas-js-runtime module a bit more!

When testing the above behavior in lambda, I am able to replicate the same duplicate app error so I don't think the issue I am facing is a blocker. It seems that the standard in a faas solution, is that there is some responsibility that user would have to avoid state issues, as you mentioned earlier. For now I am good closing this issue! I don't think there is anything that needs to be addressed given that the issue is reproducible in other solutions. Thank you all for the assistance!