Tutorial - Use Streaming in a Rust Serverless Function

This tutorial shows how to create an EDJX serverless function in Rust using the streaming feature. This function receives a streamed HTTP request from the client, and simultaneously sends a streamed HTTP response back to the client.

  • The tutorial uses a Code Sample Template. You can use code templates from the Sample Rust Repository.

  • The tutorial starts with an example Rust function code that prints Welcome to EDJX, and extends to use streams for HTTP request and HTTP response.

  • The instructions in this tutorial are for Linux and macOS.

  • The tutorial uses the EdjCLI and the EDJX VS Code extension to build and deploy the serverless function. It is also possible to build the function manually using cargo and then upload the produced WASM file to EDJX through EdjConsole.

The following are the steps involved in building the serverless function:

Prerequisites

  1. Install the latest version of Rust.

    Rust requires a C linker to be present on the system. On most Linux distributions, you need to have the gcc package installed.
  2. Open a terminal instance on your system.

  3. Navigate to the $HOME/ or ~/ directory.

  4. Install the Rust WASM target.

    rustup target add wasm32-unknown-unknown
  5. Sign Up for EDJX Account.

  6. Install EdjCLI.

  7. Install VS Code Extension.

Create the rust-function Function

  1. Open a terminal instance on your system.

  2. If you are running EdjCLI for the first time on your system, initialize its configuration using the following command:

    edjx config init
  3. Use the EdjCLI to log into the EDJX Platform.

    root@edjx:~ # edjx login
    Username: <email>
    Password: **********
    Logged in successfully
    root@edjx:~ #
  4. Select the organization to associate with the new serverless application.

    edjx config organization -i

    If no organizations display, create an organization using the EdjConsole.

  5. Create an application streaming-sample-app.

    edjx application create -n streaming-sample-app
  6. Set the streaming-sample-app application to be associated with the new function.

    edjx config application -i
  7. Create a new serverless function named rust-function inside the application and configure the parameters.

    The following example creates a new local rust-function-code directory and initializes it with a simple Rust example function template.

    edjx function init rust-function-code
    Function Name: rust-function
    ✔ WASM
    ✔ Rust
    ✔ HTTP
    ✔ 30
    ✔ 64
    Setting up project with starter files......
    Project successfully initialized in rust-function-code

    (Optional) You can view the details of the functions created using the following command:

    edjx function list
    
    
    Total functions: 1
    Name           Function ID                           Trigger  Language  Compute (GiB-Sec)  Network (B)  Requests  Organization                     Created By   Created On                         Last Updated
    rust-function  f4998cb6-3011-465f-a25b-59b29c07d9d5  HTTP     Rust      0                  0            0         CompletelyDifferentOrganization  David Sebek  2022-11-09 17:03:28.279 -0500 EST  2022-11-09 17:03:29.323 -0500 EST
  8. Change to the function directory.

    cd rust-function-code
  9. Open the directory in VS Code.

    code .

    Or

    Open VS Code and navigate to File > Open Folder and select the rust-function-code directory. The directory and files of your function display.

  10. Open the serverless_function.rs file.

    Open the serverless_function.rs file in VS Code

    The function contains the following code:

    use edjx::{info, HttpRequest, HttpResponse, StatusCode};
    
    pub fn serverless(_req: HttpRequest) -> HttpResponse {
    
        info!("Inside example function");
    
        HttpResponse::from("Welcome to EDJX".to_string())
            .set_status(StatusCode::OK)
            .set_header("Server".parse().unwrap(), "EDJX".parse().unwrap())
    }

    This function receives an HTTP request and sends an HTTP response that contains a Welcome to EDJX string in the response body.

  11. Modify the function to send the response as Welcome to EDJX! Streamed data will be echoed back.

        HttpResponse::from("Welcome to EDJX! Streamed data will be echoed back.".to_string())
            .set_status(StatusCode::OK)
            .set_header("Server".parse().unwrap(), "EDJX".parse().unwrap())
  12. Modify the lib.rs file to allow a streamed HTTP response from the serverless function.

    mod serverless_function;
    use edjx::{error, HttpRequest, HttpResponse, StatusCode};
    
    // Response stream version
    #[no_mangle]
    pub fn init() {
        let req = match HttpRequest::from_client() {
            Ok(val) => val,
            Err(e) => {
                error!("{}", e.to_string().as_str());
                HttpResponse::new()
                    .set_status(StatusCode::BAD_REQUEST)
                    .send()
                    .unwrap();
                return;
            }
        };
    
        crate::serverless_function::serverless_streaming(req);
    }
    If the function is sending HTTP response using streams, you cannot use the standard lib.rs. Use template lib.rs instead, and optionally customize the template.
  13. Update the serverless function signature in serverless_function.rs.

    pub fn serverless_streaming(mut req: HttpRequest) {
  14. In the serverless function, open a read stream from the incoming HTTP request using req.get_read_stream().

    use edjx::{info, HttpRequest, HttpResponse, StatusCode};
    
    pub fn serverless_streaming(mut req: HttpRequest) {
    
        info!("Inside example function");
    
        // Open a read stream from the request
        let read_stream = match req.get_read_stream() {
            Ok(stream) => stream,
            Err(err) => {
                error!("Could not open read stream: {}", err.to_string());
                return;
            }
        };
    
        HttpResponse::from("Welcome to EDJX! Streamed data will be echoed back.".to_string())
            .set_status(StatusCode::OK)
            .set_header("Server".parse().unwrap(), "EDJX".parse().unwrap())
  15. Open a write stream for the response, and write the message for the client to the stream. Add use::edjx::error to use error log messages.

    use edjx::{error, info, HttpRequest, HttpResponse, StatusCode};
    
    pub fn serverless_streaming(mut req: HttpRequest) {
    
        info!("Inside example function");
    
        // Open a read stream from the request
        let read_stream = match req.get_read_stream() {
            Ok(stream) => stream,
            Err(err) => {
                error!("Could not open read stream: {}", err.to_string());
                return;
            }
        };
    
        // Prepare a response
        let mut res = HttpResponse::new()
            .set_status(StatusCode::OK)
            .set_header("Serverless".parse().unwrap(), "EDJX".parse().unwrap());
    
        // Open a write stream for the response
        let mut write_stream = match res.send_streaming() {
            Ok(stream) => stream,
            Err(err) => {
                error!("Could not open write stream: {}", err);
                return;
            }
        };
    
        // Stream the message
        if let Err(err) = write_stream.write_chunk_text(
            "Welcome to EDJX! Streamed data will be echoed back.\r\n"
        ) {
            error!("Error when writing a chunk: {}", err);
            return;
        }
    }
  16. Add a loop that reads incoming chunks of data from the read stream and immediately sends any received chunk to the write stream. Add the code at the end of the function.

        }
    
        // Read all chunks from the read stream and send them to the write stream
        let mut count = 0;
        while let Some(read_result) = read_stream.read_chunk() {
            match read_result {
                Ok(bytes) => {
                    // A chunk of data was received. Send it back to the client.
                    if let Err(err) = write_stream.write_chunk_binary(bytes) {
                        error!("Error when writing a chunk: {}", err);
                        return;
                    }
                    count += 1;
                }
                Err(err) => {
                    error!("Error when reading a chunk: {}", err);
                    return;
                }
            }
        }
    }

    Alternatively, you can use the pipe_to() function, but the chunk count number will not be available.

        }
    
        // Read all chunks from the read stream and send them to the write stream
        if let Err(err) = read_stream.pipe_to(&mut write_stream) {
            error!("Error when piping streams: {}", err);
            return;
        }
    }
  17. After receiving all data, send a text summary with the number of chunks that were received.

        }
    
        // Write some statistics at the end
        if let Err(err) = write_stream.write_chunk_text(
            format!("\r\nTransmitted {} chunks.\r\n", count).as_str()
        ) {
            error!("Error when writing the summary info: {}", err);
            return;
        }
    }
  18. Close both the read and write streams.Add use::edjx::BaseStream at the beginning of the file to be able to use the close() methods on streams.

        }
    
        // Close the write stream
        if let Err(err) = write_stream.close() {
            error!("Error when closing the write stream: {}", err);
        }
    
        // Close the read stream
        if let Err(err) = read_stream.close() {
            error!("Error when closing the read stream: {}", err);
        }
    }

Build the rust-function Function

  1. With VS Code open, select the edjconfig.yaml file.

    Open the edjconfig.yaml file

  2. Click Build.

    Build the function in VS Code

    To build using the EdjCLI, issue the edjx function build command while in the function directory.
    ~/rust-function-code$ edjx function build

    The build process executes.

    To build the function into a WASM file manually, use cargo.
    ~/rust-function-code$ cargo build --release --target=wasm32-unknown-unknown
  3. When the process is successfully completed, a WASM file with the serverless function is created. The path to the WASM file is added to the edjconfig.yaml file as an artifact_path.

    Path to a WASM file in edjconfig.yaml

    By default, the WASM file is built and saved under the <function_directory>/target/wasm32-unknown-unknown/release directory.

Deploy the rust-function Function

  1. With VS Code open, click Deploy.

    Deploy the function via VS Code

    To deploy the function using the EdjCLI, issue the edjx function deploy command while in the function directory.
    ~/rust-function-code$ edjx function deploy
    To deploy the function using EdjConsole, take the WASM file and deploy it via the EdjConsole web interface.
  2. Using the EdjCLI, select the streaming-sample-app application you created.

    edjx config application -i
  3. Get information about the rust-function function that is inside the application.

    This information can also be accessed in the EdjConsole web interface.
    edjx function read rust-function
    Name                rust-function
    Function ID         f4998cb6-3011-465f-a25b-59b29c07d9d5
    Trigger             HTTP
    Language            Rust
    Runtime             WASM
    Timeout             60s
    Memory Allocated    32.0 MiB
    Compute (GiB-Sec)   0 GiB-SEC
    Network (B)         0 B
    Requests            0
    Organization        CompletelyDifferentOrganization
    Created By          David Sebek
    Created At          2022-11-09 17:03:28.279 -0500 EST
    Last Updated        2022-11-09 17:03:29.323 -0500 EST
    Execution URL       https://34e40e08-2e7e-47fb-aed5-b77123115c88.fn.edjx.net/rust-function
  4. Copy the Execution URL. The running function can be accessed via this URL.

  5. Test the function with the following curl command.

    curl -L 'https://c532061b-da57-424d-863b-b741a9f4cc58.fn.edjx.net/rust-function' -s -N -T.

    In this case, curl accepts data from user input and sends lines of user input as individual data chunks. The serverless function receives these chunks on its read stream. The function then sends the chunk to its write stream, and curl displays the chunk.

    The example run below shows both the user input to curl and the function response at the same time. One line of user input is immediately followed by an identical line of function response. It demonstrates that the data is being streamed both ways between a client and the serverless function.

    david@EDJX-MBP ~ % curl -L 'https://c532061b-da57-424d-863b-b741a9f4cc58.fn.edjx.net/rust-function' -s -N -T.
    Welcome to EDJX! Streamed data will be echoed back.
    This function
    This function
    receives
    receives
    chunks of data
    chunks of data
    and sends them back
    and sends them back
    immediately.
    immediately.
    ^D
    Transmitted 5 chunks.

You can also upload data to the EDJX storage using streams. Refer to the code sample.