Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c82a928
feat(wasi): add `wasmtime-go` as a dependency
lioia Oct 11, 2024
62afbf3
feat(wasi): implement WasiFactory for WASI code and untar functionality
lioia Oct 11, 2024
e0fc53f
feat(wasi): add new runtime `WASI_RUNTIME`
lioia Oct 11, 2024
a6f98b5
feat(wasi): separate execution based on factory type
lioia Oct 11, 2024
362e393
feat(wasi): support both docker and wasi factories
lioia Oct 11, 2024
7e22e5d
fix(wasi): rename WasiType const to upper case
lioia Oct 11, 2024
d521a1a
feat(wasi): handle URL in `src` field in etcd
lioia Oct 12, 2024
8f68087
fix(wasi): `GetFactoryFromFunction` is using `function.Function`
lioia Oct 15, 2024
cebb965
fix(wasi): create wasi engine once
lioia Oct 15, 2024
e36a4c3
feat(wasi): untar in `CopyToContainer`; add comments
lioia Oct 26, 2024
33c61f2
feat(wasi): remove files and folders when deleting runner
lioia Oct 26, 2024
de1ca1f
feat(wasi): lock on runner creation to avoid duplicates
lioia Oct 28, 2024
4b12e6b
feat(wasi): use function Handler as argv (used for Python execution)
lioia Oct 28, 2024
d90667f
fix(wasi): stdout and stderr temp file removal after execution
lioia Oct 28, 2024
8551335
feat(wasi): pass params as json string (not tested)
lioia Oct 30, 2024
5961632
feat(wasi): remove MemoryMB parameter in a Wasi function
lioia Oct 30, 2024
61b051a
fix(wasi): correctly pass handler to argv
lioia Oct 31, 2024
47f391c
feat(wasi): use RWMutex instead of Mutex; download in `CopyToContainer`
lioia Oct 31, 2024
2e17748
feat(wasi): replace locks with sync.Map and sync.Once for initialization
lioia Nov 2, 2024
79ae993
fix(wasi): improve creations and closing
lioia Nov 2, 2024
2ad59f3
fix(wasi): better detect execution error
lioia Nov 4, 2024
f788cbb
fix(wasi): moved URL download back in NewContainer
lioia Nov 5, 2024
e0cda66
fix(wasi): remove unused WasiConfig declaration
lioia Nov 5, 2024
2b26845
feat(wasi): enable modules cache and threads support
lioia Nov 6, 2024
23c68a9
fix(wasi): switch to component
lioia Nov 7, 2024
e8f96d8
feat(wasi): add network support for wasmtime CLI
lioia Nov 8, 2024
08461d8
docs(wasi): WASI function creation; internal docs for future references
lioia Nov 23, 2024
61f9f4e
chore: Merge branch 'main' of github.com:lioia/serverledge into wasi
lioia Dec 9, 2024
9e4fa46
fix(wasi): correctly compute InitTime
lioia Dec 17, 2024
5f598a9
fix(wasi): correctly compute init time (with module instantiation)
lioia Dec 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ BIN=bin
all: serverledge executor serverledge-cli lb

serverledge:
CGO_ENABLED=0 GOOS=linux go build -o $(BIN)/$@ cmd/$@/main.go
GOOS=linux go build -o $(BIN)/$@ cmd/$@/main.go

lb:
CGO_ENABLED=0 GOOS=linux go build -o $(BIN)/$@ cmd/$@/main.go
GOOS=linux go build -o $(BIN)/$@ cmd/$@/main.go

serverledge-cli:
CGO_ENABLED=0 GOOS=linux go build -o $(BIN)/$@ cmd/cli/main.go
GOOS=linux go build -o $(BIN)/$@ cmd/cli/main.go

executor:
CGO_ENABLED=0 GOOS=linux go build -o $(BIN)/$@ cmd/$@/executor.go
Expand Down
54 changes: 54 additions & 0 deletions docs/writing-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,60 @@ Available runtime: `nodejs17` (NodeJS 17)
Specify the handler as `<script_file_name>.js` (e.g., `myfile.js`).
An example is given in `examples/sieve.js`.

## WebAssembly/WASI

Serverledge supports the execution of WebAssembly modules through WASI using
[wasmtime-go](https://github.com/bytecodealliance/wasmtime-go).

A WebAssembly function can be defined in Serverledge by specifying the following
arguments to `serverledge-cli`:

- `runtime`: `wasi`
- `src`: the path of the `.wasm` file. If the file is larger than 2MB (`etcd`
message size limit), it has to point to a URL of a `.tar` containing the
`.wasm` file
- `custom_image`: the name of the `.wasm` file inside the `.tar` file

When running a function written in Python, the `handler` argument is **required**
and it has to point to the `.py` file inside the `.tar` passed in `src`.

NOTE: if the WebAssembly module is a component, the execution of the function
will be handled by the `wasmtime` CLI which has to be installed and its
installation path added to the `PATH` environment variable.

### Examples

**Wasm file smaller than 2MB**: assuming we have a `hello.wasm` file inside the
`/home/user/code/` directory

A function can be created using this command:

serverledge-cli create -f func-name \
--runtime wasi \
--src /home/user/code/hello.wasm \
--custom_image hello

**Python** (using the official build): assuming the `.tar` is hosted on
`localhost:8000/python.tar` and inside the `.tar` file there is a `func.py`

At the time of writing, the official build is provided by the maintainer of the
WASI platform in Python at the following [link](https://github.com/brettcannon/cpython-wasi-build/releases/tag/v3.13.0)

Serverledge assumes the structure of the `python.tar` file to be the following:

- `func.py`: this can also be in a sub-directory and is specified in the
`handler` argument of the function creation
- `python.wasm`
- `lib/`

A function can be created using this command:

serverledge-cli create -f func-name \
--runtime wasi \
--src http://localhost:8000/python.tar \
--custom_image python \
--handler func.py

## Custom function runtimes

Follow [these instructions](./custom_runtime.md).
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ toolchain go1.22.5

require (
github.com/LK4D4/trylock v0.0.0-20191027065348-ff7e133a5c54
github.com/bytecodealliance/wasmtime-go/v25 v25.0.0
github.com/docker/docker v20.10.12+incompatible
github.com/hexablock/vivaldi v0.0.0-20180727225019-07adad3f2b5f
github.com/labstack/echo/v4 v4.6.1
Expand All @@ -17,7 +18,9 @@ require (
go.opentelemetry.io/otel v1.28.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0
go.opentelemetry.io/otel/sdk v1.28.0
go.opentelemetry.io/otel/trace v1.28.0
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
golang.org/x/sys v0.21.0
)

require (
Expand Down Expand Up @@ -65,12 +68,10 @@ require (
go.etcd.io/etcd/api/v3 v3.5.1 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bytecodealliance/wasmtime-go/v25 v25.0.0 h1:ZTn4Ho+srrk0466ugqPfTDCITczsWdT48A0ZMA/TpRU=
github.com/bytecodealliance/wasmtime-go/v25 v25.0.0/go.mod h1:8mMIYQ92CpVDwXPIb6udnhtFGI3vDZ/937cGeQr5I68=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
Expand Down Expand Up @@ -446,7 +448,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
Expand Down Expand Up @@ -506,6 +507,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
Expand Down Expand Up @@ -1093,8 +1096,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
Expand Down
7 changes: 6 additions & 1 deletion internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,13 @@ func CreateFunction(c echo.Context) error {

log.Printf("New request: creation of %s\n", f.Name)

if f.Runtime == container.WASI_RUNTIME {
// Dropping memory requirements because it cannot be enforced in Wasi
f.MemoryMB = 0
}

// Check that the selected runtime exists
if f.Runtime != container.CUSTOM_RUNTIME {
if f.Runtime != container.CUSTOM_RUNTIME && f.Runtime != container.WASI_RUNTIME {
_, ok := container.RuntimeToInfo[f.Runtime]
if !ok {
return c.JSON(http.StatusNotFound, "Invalid runtime.")
Expand Down
17 changes: 13 additions & 4 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"

Expand Down Expand Up @@ -195,10 +196,18 @@ func create(cmd *cobra.Command, args []string) {

var encoded string
if runtime != "custom" {
srcContent, err := readSourcesAsTar(src)
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(3)
var srcContent []byte
u, err := url.ParseRequestURI(src)
if err == nil && u.Scheme != "" && u.Host != "" {
// src is a URL
srcContent = []byte(src)
} else {
// src is a folder; a tar has to be created to be uploaded to etcd
srcContent, err = readSourcesAsTar(src)
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(3)
}
}
encoded = base64.StdEncoding.EncodeToString(srcContent)
} else {
Expand Down
156 changes: 148 additions & 8 deletions internal/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,44 @@ import (
"io"
"log"
"net/http"
"net/url"
"os/exec"
"strings"
"time"

"github.com/grussorusso/serverledge/internal/executor"
"github.com/grussorusso/serverledge/internal/function"
)

// NewContainer creates and starts a new container.
func NewContainer(image, codeTar string, opts *ContainerOptions) (ContainerID, error) {
func NewContainer(image, codeTar string, opts *ContainerOptions, f *function.Function) (ContainerID, error) {
cf := GetFactoryFromFunction(f)
contID, err := cf.Create(image, opts)
if err != nil {
log.Printf("Failed container creation\n")
return "", err
}

if len(codeTar) > 0 {
var r io.Reader
// Decoding codeTar
decodedCode, _ := base64.StdEncoding.DecodeString(codeTar)
err = cf.CopyToContainer(contID, bytes.NewReader(decodedCode), "/app/")
// Check if decoded src is a url
u, err := url.ParseRequestURI(string(decodedCode))
if err == nil && u.Scheme != "" && u.Host != "" {
// codeTar is an URL; it has to be downloaded
resp, err := http.Get(string(decodedCode))
if err != nil {
log.Printf("Failed to download code %s", decodedCode)
return "", err
}
defer resp.Body.Close()
r = resp.Body
} else {
// assuming decodedCode is base64 encoded tar
r = bytes.NewReader(decodedCode)
}
err = cf.CopyToContainer(contID, r, "/app/")
if err != nil {
log.Printf("Failed code copy\n")
return "", err
Expand All @@ -38,10 +60,128 @@ func NewContainer(image, codeTar string, opts *ContainerOptions) (ContainerID, e
return contID, nil
}

func Execute(contID ContainerID, req *executor.InvocationRequest, f *function.Function) (*executor.InvocationResult, time.Duration, error) {
if f.Runtime == WASI_RUNTIME {
return wasiExecute(contID, req)
} else {
return dockerExecute(contID, req)
}
}

func wasiExecute(contID ContainerID, req *executor.InvocationRequest) (*executor.InvocationResult, time.Duration, error) {
wf := factories[WASI_FACTORY_KEY].(*WasiFactory)
wrValue, _ := wf.runners.Load(contID)
wr := wrValue.(*wasiRunner)

if wr.wasiType == WASI_TYPE_UNDEFINED {
return nil, 0, fmt.Errorf("Unrecognized WASI Type")
}

var paramsBytes []byte
if req.Params != nil {
var err error
paramsBytes, err = json.Marshal(req.Params)
if err != nil {
return nil, 0, fmt.Errorf("Failed to convert params to JSON: %v", err)
}
}

res := &executor.InvocationResult{Success: false}
t0 := time.Now()
var invocationWait time.Duration
if wr.wasiType == WASI_TYPE_MODULE {
// Create a new Wasi Configuration
wcc, err := wr.BuildStore(contID, wf.engine, req.Handler, string(paramsBytes))
if err != nil {
return nil, time.Now().Sub(t0), err
}
defer wcc.Close()

// Create an instance of the module
instance, err := wr.linker.Instantiate(wcc.store, wr.module)
if err != nil {
return nil, time.Now().Sub(t0), fmt.Errorf("Failed to instantiate WASI module: %v", err)
}

// Get the _start function (entrypoint of any wasm module)
start := instance.GetFunc(wcc.store, "_start")
if start == nil {
return nil, time.Now().Sub(t0), fmt.Errorf("WASI Module does not have a _start function")
}

invocationWait = time.Now().Sub(t0)
// Call the _start function
if _, err := start.Call(wcc.store); err != nil &&
!strings.Contains(err.Error(), "exit status 0") {
return nil, invocationWait, fmt.Errorf("Failed to run WASI module: %v", err)
}

// Read stdout from the temp file
stdout, err := io.ReadAll(wcc.stdout)
if err != nil {
return nil, invocationWait, fmt.Errorf("Failed to read stdout for WASI: %v", err)
}

// Read stderr from the temp file
stderr, err := io.ReadAll(wcc.stderr)
if err != nil {
return nil, invocationWait, fmt.Errorf("Failed to read stderr for WASI: %v", err)
}

// Populate result
res.Success = true
res.Result = string(stdout)
if req.ReturnOutput {
res.Output = fmt.Sprintf("%s\n%s", string(stdout), string(stderr))
}
} else if wr.wasiType == WASI_TYPE_COMPONENT {
// Create wasmtime CLI command
args := append(wr.cliArgs, wr.mount+req.Handler)
if len(paramsBytes) > 0 {
args = append(args, string(paramsBytes))
}
execCmd := exec.Command("wasmtime", args...)

// Save stdout and stderr to another buffer
var stdoutBuffer, stderrBuffer bytes.Buffer
execCmd.Stdout = &stdoutBuffer
execCmd.Stderr = &stderrBuffer

invocationWait = time.Now().Sub(t0)

// Execute wasmtime CLI
err := execCmd.Run()
if err != nil {
log.Printf("wasmtime failed with %v\n", err)
}

// Read stdout from temporary buffer
stdout, err := io.ReadAll(&stdoutBuffer)
if err != nil {
log.Printf("Failed to read stdout: %v", err)
}

// Read stderr from temporary buffer
stderr, err := io.ReadAll(&stderrBuffer)
if err != nil {
log.Printf("Failed to read stderr: %v", err)
}

// Create response
res.Success = err == nil
res.Result = string(stdout)
if req.ReturnOutput {
res.Output = fmt.Sprintf("%s\n%s", string(stdout), string(stderr))
}
}

return res, invocationWait, nil
}

// Execute interacts with the Executor running in the container to invoke the
// function through a HTTP request.
func Execute(contID ContainerID, req *executor.InvocationRequest) (*executor.InvocationResult, time.Duration, error) {
ipAddr, err := cf.GetIPAddress(contID)
func dockerExecute(contID ContainerID, req *executor.InvocationRequest) (*executor.InvocationResult, time.Duration, error) {
ipAddr, err := factories[DOCKER_FACTORY_KEY].GetIPAddress(contID)
if err != nil {
return nil, 0, fmt.Errorf("Failed to retrieve IP address for container: %v", err)
}
Expand Down Expand Up @@ -70,12 +210,12 @@ func Execute(contID ContainerID, req *executor.InvocationRequest) (*executor.Inv
return response, waitDuration, nil
}

func GetMemoryMB(id ContainerID) (int64, error) {
return cf.GetMemoryMB(id)
func GetMemoryMB(id ContainerID, f *function.Function) (int64, error) {
return GetFactoryFromFunction(f).GetMemoryMB(id)
}

func Destroy(id ContainerID) error {
return cf.Destroy(id)
func Destroy(id ContainerID, f *function.Function) error {
return GetFactoryFromFunction(f).Destroy(id)
}

func sendPostRequestWithRetries(url string, body *bytes.Buffer) (*http.Response, time.Duration, error) {
Expand Down
5 changes: 4 additions & 1 deletion internal/container/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ func InitDockerContainerFactory() *DockerFactory {
}

dockerFact := &DockerFactory{cli, ctx}
cf = dockerFact
if factories == nil {
factories = make(map[string]Factory)
}
factories[DOCKER_FACTORY_KEY] = dockerFact
return dockerFact
}

Expand Down
Loading