├── .dockerignore ├── .github └── workflows │ └── makefile.yml ├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── cmd ├── cli │ └── main.go ├── executor │ └── executor.go ├── lb │ └── main.go └── serverledge │ └── main.go ├── docs ├── api.md ├── configuration.md ├── custom_runtime.md ├── executor.md ├── logo.png ├── metrics.md ├── tracing.md ├── workflows.md └── writing-functions.md ├── examples ├── c++ │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── example.json │ ├── function.cpp │ └── rapidjson │ │ ├── allocators.h │ │ ├── cursorstreamwrapper.h │ │ ├── document.h │ │ ├── encodedstream.h │ │ ├── encodings.h │ │ ├── error │ │ ├── en.h │ │ └── error.h │ │ ├── filereadstream.h │ │ ├── filewritestream.h │ │ ├── fwd.h │ │ ├── internal │ │ ├── biginteger.h │ │ ├── clzll.h │ │ ├── diyfp.h │ │ ├── dtoa.h │ │ ├── ieee754.h │ │ ├── itoa.h │ │ ├── meta.h │ │ ├── pow10.h │ │ ├── regex.h │ │ ├── stack.h │ │ ├── strfunc.h │ │ ├── strtod.h │ │ └── swap.h │ │ ├── istreamwrapper.h │ │ ├── memorybuffer.h │ │ ├── memorystream.h │ │ ├── msinttypes │ │ ├── inttypes.h │ │ └── stdint.h │ │ ├── ostreamwrapper.h │ │ ├── pointer.h │ │ ├── prettywriter.h │ │ ├── rapidjson.h │ │ ├── reader.h │ │ ├── schema.h │ │ ├── stream.h │ │ ├── stringbuffer.h │ │ ├── uri.h │ │ └── writer.h ├── classes_config.json ├── config_edgeCloud.yaml ├── config_example.yaml ├── config_example_2.yaml ├── config_example_cloud.yaml ├── custom_hello │ ├── Dockerfile │ └── function.py ├── double.py ├── fibonacci.py ├── fibonacciNout.py ├── grep.py ├── grepInput.json ├── hash_string.py ├── hello.js ├── hello.py ├── inc.js ├── inc.py ├── input.json ├── input2.json ├── isprime.py ├── isprimeWithNumber.py ├── jsonschema │ ├── Dockerfile │ ├── README.md │ ├── executor.py │ ├── function.py │ └── input.json ├── local_offloading │ ├── README.md │ ├── confCloud.yaml │ └── confEdge.yaml ├── noop.py ├── prometheus_config.yml ├── sieve.js ├── sleeper.py ├── summarize.py ├── summarizeInput.json ├── wordCount.js ├── wordCountInput.json └── workflow-simple.json ├── go.mod ├── go.sum ├── images ├── base-alpine │ └── Dockerfile ├── nodejs17ng │ ├── Dockerfile │ └── executor.js └── python310 │ ├── Dockerfile │ └── executor.py ├── internal ├── api │ ├── api.go │ ├── server.go │ └── workflow.go ├── asl │ ├── catch.go │ ├── choice.go │ ├── choice_rules.go │ ├── fail.go │ ├── formula.go │ ├── intrinsics.go │ ├── json.go │ ├── map.go │ ├── parallel.go │ ├── pass.go │ ├── path.go │ ├── payload_template.go │ ├── retry.go │ ├── state.go │ ├── state_machine.go │ ├── succeed.go │ ├── task.go │ └── wait.go ├── cache │ ├── cache.go │ └── cache_handler.go ├── cli │ └── cli.go ├── client │ └── types.go ├── config │ ├── config.go │ ├── keys.go │ └── types.go ├── container │ ├── container.go │ ├── docker.go │ ├── factory.go │ └── runtimes.go ├── executor │ ├── constants.go │ ├── server.go │ └── types.go ├── function │ ├── function.go │ ├── request.go │ ├── signature.go │ └── types.go ├── lb │ └── lb.go ├── metrics │ └── metrics.go ├── node │ ├── janitor_handler.go │ ├── node.go │ └── pool.go ├── registration │ ├── edgeNetUDPDiscovery.go │ ├── registry.go │ ├── registryLog.go │ └── types.go ├── scheduling │ ├── async.go │ ├── cloudonly_policy.go │ ├── edgeCloudPolicy.go │ ├── edgeOnlyPolicy.go │ ├── execution.go │ ├── offloading.go │ ├── policy.go │ ├── policy_default.go │ ├── queue.go │ ├── scheduler.go │ └── types.go ├── telemetry │ └── otel.go ├── test │ ├── api_test.go │ ├── asl │ │ ├── bad.json │ │ ├── choice_boolexpr.json │ │ ├── choice_datastring.json │ │ ├── choice_datatestexpr.json │ │ ├── choice_numeq_succeed_fail.json │ │ ├── machine.json │ │ ├── mixed_sequence.json │ │ ├── sequence.json │ │ ├── simple.json │ │ ├── test.json │ │ ├── test2.json │ │ └── test3.json │ ├── aslparser_test.go │ ├── condition_test.go │ ├── main_test.go │ ├── parse_choice_test.go │ ├── parse_task_test.go │ ├── partial_data_test.go │ ├── progress_test.go │ ├── signature_test.go │ ├── types_test.go │ ├── util.go │ ├── workflow_integration_test.go │ └── workflow_test.go ├── types │ └── comparable.go └── workflow │ ├── asl.go │ ├── async.go │ ├── builder.go │ ├── choice_task.go │ ├── conditions.go │ ├── end_task.go │ ├── fail_task.go │ ├── fanin_task.go │ ├── fanout_task.go │ ├── partial_data.go │ ├── pass_task.go │ ├── progress.go │ ├── report.go │ ├── request.go │ ├── scheduler.go │ ├── simple_task.go │ ├── start_task.go │ ├── succeed_task.go │ ├── task.go │ └── workflow.go ├── prometheus.yml ├── scripts ├── compositions │ ├── create-composition.sh │ ├── delete-composition.sh │ ├── get-composition.sh │ ├── invoke-async-composition.sh │ ├── invoke-composition.sh │ └── poll-composition.sh ├── defrag-etcd.sh ├── experiments │ ├── experiment1.sh │ └── experiment2.sh ├── functions │ ├── create-function.sh │ ├── delete-function.sh │ ├── get-functions.sh │ ├── invoke-async-function.sh │ ├── invoke-function.sh │ └── poll-invocation.sh ├── list-etcd-keys.sh ├── remove-etcd.sh ├── simple-benchmark.sh ├── start-etcd.sh └── start-prometheus.sh └── utils ├── convert.go ├── errors.go ├── etcd.go ├── http.go ├── networking.go ├── tar.go └── testing.go /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .idea/ 3 | __pycache__* 4 | docs/ 5 | examples/ 6 | -------------------------------------------------------------------------------- /.github/workflows/makefile.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Setup Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: "1.21.0" 19 | 20 | - uses: actions/checkout@v2 21 | 22 | - name: Install dependencies 23 | run: make 24 | 25 | - name: Test 26 | run: make test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .idea/ 3 | .run/ 4 | __pycache__* 5 | images/*/executor 6 | *.backup 7 | default.etcd 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gabriele Russo Russo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN=bin 2 | GO=go 3 | all: serverledge executor serverledge-cli lb 4 | 5 | serverledge: 6 | $(GO) build -o $(BIN)/$@ cmd/$@/main.go 7 | 8 | lb: 9 | CGO_ENABLED=0 $(GO) build -o $(BIN)/$@ cmd/$@/main.go 10 | 11 | serverledge-cli: 12 | CGO_ENABLED=0 $(GO) build -o $(BIN)/$@ cmd/cli/main.go 13 | 14 | executor: 15 | CGO_ENABLED=0 $(GO) build -o $(BIN)/$@ cmd/$@/executor.go 16 | 17 | DOCKERHUB_USER=grussorusso 18 | images: image-python310 image-nodejs17ng image-base 19 | image-python310: 20 | docker build -t $(DOCKERHUB_USER)/serverledge-python310 -f images/python310/Dockerfile . 21 | image-base: 22 | docker build -t $(DOCKERHUB_USER)/serverledge-base -f images/base-alpine/Dockerfile . 23 | image-nodejs17ng: 24 | docker build -t $(DOCKERHUB_USER)/serverledge-nodejs17ng -f images/nodejs17ng/Dockerfile . 25 | 26 | push-images: 27 | docker push $(DOCKERHUB_USER)/serverledge-python310 28 | docker push $(DOCKERHUB_USER)/serverledge-base 29 | docker push $(DOCKERHUB_USER)/serverledge-nodejs17ng 30 | 31 | test: 32 | go test -v ./... 33 | 34 | .PHONY: serverledge serverledge-cli lb executor test images 35 | 36 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/serverledge-faas/serverledge/internal/cli" 9 | "github.com/serverledge-faas/serverledge/internal/config" 10 | ) 11 | 12 | func main() { 13 | config.ReadConfiguration("") 14 | 15 | // Set defaults 16 | cli.ServerConfig.Host = "127.0.0.1" 17 | cli.ServerConfig.Port = config.GetInt("api.port", 1323) 18 | 19 | // Check for environment variables 20 | if envHost, ok := os.LookupEnv("SERVERLEDGE_HOST"); ok { 21 | cli.ServerConfig.Host = envHost 22 | } 23 | if envPort, ok := os.LookupEnv("SERVERLEDGE_PORT"); ok { 24 | if iPort, err := strconv.Atoi(envPort); err == nil { 25 | cli.ServerConfig.Port = iPort 26 | } else { 27 | log.Fatalf("Invalid port number: %s\n", envPort) 28 | } 29 | } 30 | 31 | cli.Init() 32 | } 33 | -------------------------------------------------------------------------------- /cmd/executor/executor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/serverledge-faas/serverledge/internal/executor" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/invoke", executor.InvokeHandler) 13 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", executor.DEFAULT_EXECUTOR_PORT), nil)) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/lb/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "time" 9 | 10 | "golang.org/x/net/context" 11 | 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo/v4/middleware" 14 | "github.com/serverledge-faas/serverledge/internal/config" 15 | "github.com/serverledge-faas/serverledge/internal/lb" 16 | "github.com/serverledge-faas/serverledge/internal/registration" 17 | "github.com/serverledge-faas/serverledge/utils" 18 | ) 19 | 20 | func registerTerminationHandler(e *echo.Echo) { 21 | c := make(chan os.Signal) 22 | signal.Notify(c, os.Interrupt) 23 | 24 | go func() { 25 | select { 26 | case sig := <-c: 27 | fmt.Printf("Got %s signal. Terminating...\n", sig) 28 | 29 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 30 | defer cancel() 31 | if err := e.Shutdown(ctx); err != nil { 32 | e.Logger.Fatal(err) 33 | } 34 | 35 | os.Exit(0) 36 | } 37 | }() 38 | } 39 | 40 | func main() { 41 | configFileName := "" 42 | if len(os.Args) > 1 { 43 | configFileName = os.Args[1] 44 | } 45 | config.ReadConfiguration(configFileName) 46 | 47 | // TODO: split Area in Region + Type (e.g., cloud/lb/edge) 48 | region := config.GetString(config.REGISTRY_AREA, "ROME") 49 | registry := ®istration.Registry{Area: "lb/" + region} 50 | 51 | defaultAddressStr := "127.0.0.1" 52 | address, err := utils.GetOutboundIp() 53 | if err == nil { 54 | defaultAddressStr = address.String() 55 | } 56 | registration.RegisteredLocalIP = config.GetString(config.API_IP, defaultAddressStr) 57 | 58 | if _, err := registry.RegisterToEtcd(); err != nil { 59 | log.Printf("Could not register to Etcd: %v\n", err) 60 | } 61 | 62 | e := echo.New() 63 | e.HideBanner = true 64 | e.Use(middleware.Recover()) 65 | 66 | // Register a signal handler to cleanup things on termination 67 | registerTerminationHandler(e) 68 | 69 | lb.StartReverseProxy(e, region) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/serverledge/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "time" 11 | 12 | "github.com/serverledge-faas/serverledge/internal/api" 13 | "github.com/serverledge-faas/serverledge/internal/config" 14 | "github.com/serverledge-faas/serverledge/internal/metrics" 15 | "github.com/serverledge-faas/serverledge/internal/node" 16 | "github.com/serverledge-faas/serverledge/internal/registration" 17 | "github.com/serverledge-faas/serverledge/internal/scheduling" 18 | "github.com/serverledge-faas/serverledge/internal/telemetry" 19 | "github.com/serverledge-faas/serverledge/utils" 20 | 21 | "github.com/labstack/echo/v4" 22 | ) 23 | 24 | func main() { 25 | configFileName := "" 26 | if len(os.Args) > 1 { 27 | configFileName = os.Args[1] 28 | } 29 | config.ReadConfiguration(configFileName) 30 | 31 | //setting up cache parameters 32 | api.CacheSetup() 33 | 34 | // register to etcd, this way server is visible to the others under a given local area 35 | registry := new(registration.Registry) 36 | isInCloud := config.GetBool(config.IS_IN_CLOUD, false) 37 | if isInCloud { 38 | registry.Area = "cloud/" + config.GetString(config.REGISTRY_AREA, "ROME") 39 | } else { 40 | registry.Area = config.GetString(config.REGISTRY_AREA, "ROME") 41 | } 42 | // before register checkout other servers into the local area 43 | //todo use this info later on; future work with active remote server selection 44 | _, err := registry.GetAll(true) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | defaultAddressStr := "127.0.0.1" 50 | address, err := utils.GetOutboundIp() 51 | if err == nil { 52 | defaultAddressStr = address.String() 53 | } 54 | registration.RegisteredLocalIP = config.GetString(config.API_IP, defaultAddressStr) 55 | 56 | myKey, err := registry.RegisterToEtcd() 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | node.NodeIdentifier = myKey 61 | 62 | go metrics.Init() 63 | 64 | if config.GetBool(config.TRACING_ENABLED, false) { 65 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 66 | defer stop() 67 | 68 | tracesOutfile := config.GetString(config.TRACING_OUTFILE, "") 69 | if len(tracesOutfile) < 1 { 70 | tracesOutfile = fmt.Sprintf("traces-%s.json", time.Now().Format("20060102-150405")) 71 | } 72 | log.Printf("Enabling tracing to %s\n", tracesOutfile) 73 | otelShutdown, err := telemetry.SetupOTelSDK(ctx, tracesOutfile) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | // Handle shutdown properly so nothing leaks. 78 | defer func() { 79 | err = errors.Join(err, otelShutdown(context.Background())) 80 | }() 81 | } 82 | 83 | e := echo.New() 84 | 85 | // Register a signal handler to cleanup things on termination 86 | api.RegisterTerminationHandler(registry, e) 87 | 88 | schedulingPolicy := api.CreateSchedulingPolicy() 89 | go scheduling.Run(schedulingPolicy) 90 | 91 | if !isInCloud { 92 | err = registration.InitEdgeMonitoring(registry) 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | } 97 | 98 | api.StartAPIServer(e) 99 | 100 | } 101 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration # 2 | 3 | This page provides a (partial) list of configuration options that can 4 | be specified in Serverledge configuration files. 5 | All the supported configuration keys are defined in `internal/config/keys.go`. 6 | 7 | ## Configuration files 8 | 9 | You can provide a configuration file using YAML or TOML syntax. Depending on the 10 | chosen format, the default file name will be `serverledge-conf.yaml` or 11 | `serverledge-conf.toml`. The file can be either placed in `/etc/serverledge`, 12 | in the user `$HOME` directory, or in the working directory where the server is 13 | started. 14 | 15 | Alternatively, you can indicate a specific configuration file when starting the 16 | server: 17 | 18 | $ bin/serverledge 19 | 20 | 21 | ## Frequently used options 22 | 23 | | Configuration key | Description | Example value(s) | 24 | |--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------| 25 | | `etcd.address` | Hostname and port of the Etcd server acting as the Global Registry. | `127.0.0.1:2379` | 26 | | `api.port` | Port number for the API server. | 1323 | 27 | | `cloud.server.url` | URL prefix for the remote Cloud node API. | `http://127.0.0.1:1326` | 28 | | `factory.images.refresh` | Forces function runtime container images to be pulled from the Internet the first time they are used (to update them), even if they are available on the host. | `true` | 29 | | `container.pool.memory` | Maximum amount of memory (in MB) that the container pool can use (must be not greater than the total memory available in the host). | 4096 | 30 | | `janitor.interval` | Activation interval (in seconds) for the janitor thread that checks for expired containers. | 60 | 31 | | `container.expiration` | Expiration time (in seconds) for idle containers. | 600 | 32 | | `registry.area` | Geographic area where this node is located. | `ROME` | 33 | | `registry.udp.port` | UPD port used for peer-to-peer Edge monitoring. | | 34 | | `scheduler.policy` | Scheduling policy to use. Possible values: `default`, `localonly`, `edgeonly`, `cloudonly`. | | 35 | 36 | 49 | -------------------------------------------------------------------------------- /docs/custom_runtime.md: -------------------------------------------------------------------------------- 1 | Two approaches are available to prepare a custom container image containing 2 | your function code, as explained below. 3 | 4 | 5 | 6 | ## Custom image (the easy way) 7 | 8 | The easiest way to build a custom function image is by leveraging the 9 | Serverledge base runtime image, i.e., `grussorusso/serverledge-base`. 10 | This image contains a simple implementation of the [Executor](https://github.com/serverledge-faas/serverledge/blob/main/docs/executor.md) 11 | server. When the function is invoked, the Executor runs a user-specified 12 | command as a new process and sets a few environment variables that may be 13 | used by the called process: 14 | 15 | - `PARAMS_FILE`: path of a file containing JSON-marshaled function parameters 16 | - `RESULT_FILE`: name of the file where the function must write its JSON-encoded result 17 | - `CONTEXT`: (optional) a JSON-encoded representation of the execution context 18 | 19 | You can write a `Dockerfile` as follows to build your own runtime image, e.g.: 20 | 21 | FROM grussorusso/serverledge-base as BASE 22 | 23 | # Extend any image you want, e.g.; 24 | FROM tensorflow/tensorflow:latest 25 | 26 | # Required: install the executor as /executor 27 | COPY --from=BASE /executor / 28 | CMD /executor 29 | 30 | # Required: this is the command representing your function 31 | ENV CUSTOM_CMD "python /function.py" 32 | 33 | # Install your code and any dependency, e.g.: 34 | RUN pip3 install pillow 35 | COPY function.py / 36 | # ... 37 | 38 | A complete [example](../examples/c++/README.md) is provided for C++. 39 | 40 | ## Custom image (the better way) 41 | 42 | For higher efficiency, instead of using the default Executor implementation, 43 | you can define your image from scratch rewriting/extending the 44 | `Dockerfile` and code used to build Serverledge runtime images. 45 | For instance, if you want to create a custom image for a Python-based function, 46 | you may write your own `Dockerfile` based on the content of `/images/pythonXXX/`. 47 | 48 | By doing so, you get rid of some process creation overheads, as 49 | your function is directly called upon arrival of invocation requests. 50 | 51 | ## Using a custom image 52 | 53 | The new function can be created using the CLI interface, setting `custom` as the desired runtime and 54 | specifying the `custom_image` parameter. 55 | 56 | bin/serverledge-cli create -function myfunc -memory 256 -runtime custom -custom_image MY_IMAGE_TAG 57 | 58 | ### Example 59 | The `examples/jsonschema` directory of the repository provides example files on 60 | how to build a custom image for a Python function requiring additional 61 | libraries. 62 | -------------------------------------------------------------------------------- /docs/executor.md: -------------------------------------------------------------------------------- 1 | Functions are executed within containers. In the following, we will describe 2 | how incoming requests are served within containers, assuming that a warm 3 | container for the function exists. 4 | 5 | Each function container must run an **Executor** server, which listens for 6 | HTTP requests on port `8080` (by default). 7 | 8 | When a function request is scheduled for local execution within a warm container, 9 | an invocation request is sent to the Executor as follows: 10 | 11 | - URL: `:/invoke` 12 | 13 | - Method: `POST` 14 | 15 | - Body: an `executor.InvocationRequest` (JSON-encoded) 16 | 17 | - Response (on success): an `executor.InvocationResult` (JSON-encoded) 18 | 19 | An `InvocationRequest` has the following fields: 20 | 21 | ``` 22 | type InvocationRequest struct { 23 | Command []string 24 | Params map[string]interface{} 25 | Handler string 26 | HandlerDir string 27 | ReturnOutput bool 28 | } 29 | ``` 30 | 31 | - `Command` (runtime-dependent; optional, depending on the Executor implementation): the 32 | command that the Executor has to run upon reception of a new request. E.g., 33 | for a Python runtime, it may be set as `python /entrypoint.py`. 34 | 35 | - `Params`: user-specified function parameters. 36 | 37 | - `Handler` (runtime-dependent): identifier of the function to be executed. 38 | E.g., for Python runtimes, `.`. 39 | 40 | - `HandlerDir`: directory where the function code has been copied. 41 | 42 | - `ReturnOutput`: whether function standard output and error should be returned. 43 | 44 | The following object is returned upon function completion (or failure): 45 | 46 | ``` 47 | type InvocationResult struct { 48 | Success bool 49 | Result string 50 | Output string 51 | } 52 | ``` 53 | 54 | - `Success`: whether the function has been successfully executed. 55 | 56 | - `Result`: what the function returned. 57 | 58 | - `Output`: function combined std. output and error (if captured) 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverledge-faas/serverledge/6bf296cf7f1bfbe26bf863c97b604ce105867bdb/docs/logo.png -------------------------------------------------------------------------------- /docs/metrics.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | The metrics system must be enabled via `metrics.enabled`. 4 | If enabled, metrics are exposed at `http://localhost:2112/metrics`. 5 | 6 | You can check that the metrics system is working without starting a Prometheus 7 | server: 8 | 9 | $ curl 127.0.0.1:2112/metrics 10 | 11 | 12 | ## Available metrics 13 | 14 | A few metrics are currently exposed (just for demonstration purposes): 15 | 16 | - `sedge_completed_total`: number of completed invocations (Counter, per function) 17 | - `sedge_exectime`: execution time for each function (Histogram, per function) 18 | 19 | 20 | ## Prometheus Integration 21 | 22 | Various Prometheus configurations can be considered to scrape Serverledge 23 | metrics: 24 | 25 | - A centralized Prometheus server in the Cloud (likely not scalable...) 26 | - A Prometheus server in each Edge zone 27 | - A Prometheus server in the Cloud with a Prometheus Agent on each Serverledge 28 | node (details below) 29 | 30 | ### Example: Prometheus Agent + Cloud 31 | 32 | As regards the last option, it requires Prometheus instances to use the 33 | following (minimal) configuration. 34 | 35 | In the Serverledge node, 36 | Prometheus must be started with `--enable-feature=agent` and the following 37 | lines in the configuration: 38 | 39 | remote_write: 40 | - url: "http://:9091/api/v1/write" 41 | 42 | Example configuration: 43 | 44 | global: 45 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 46 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 47 | # scrape_timeout is set to the global default (10s). 48 | 49 | # A scrape configuration containing exactly one endpoint to scrape: 50 | scrape_configs: 51 | - job_name: "serverledge" 52 | # metrics_path defaults to '/metrics' 53 | # scheme defaults to 'http'. 54 | static_configs: 55 | - targets: [":2112"] 56 | 57 | remote_write: 58 | - url: "http://:9091/api/v1/write" 59 | 60 | In the Cloud, Prometheus must be started with `--web.enable-remote-write-receiver`. 61 | Example configuration: 62 | 63 | global: 64 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 65 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 66 | 67 | Example script to launch both Prometheus instances on the same host (for 68 | testing): 69 | 70 | docker run \ 71 | --name prom \ 72 | -d \ 73 | -p 9090:9090 \ 74 | -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \ 75 | prom/prometheus --enable-feature=agent \ 76 | --config.file=/etc/prometheus/prometheus.yml 77 | 78 | docker run \ 79 | --name promRemote \ 80 | -d\ 81 | \ 82 | -p 9091:9090 \ 83 | -v $(pwd)/prometheus_remote.yml:/etc/prometheus/prometheus.yml \ 84 | prom/prometheus --web.enable-remote-write-receiver \ 85 | --config.file=/etc/prometheus/prometheus.yml 86 | 87 | ### References 88 | 89 | - [Prometheus Agent Mode](https://prometheus.io/blog/2021/11/16/agent/) 90 | - [Prometheus + Go](https://prometheus.io/docs/guides/go-application/) 91 | -------------------------------------------------------------------------------- /docs/tracing.md: -------------------------------------------------------------------------------- 1 | Serverledge relies on [OpenTelemetry](https://opentelemetry.io) for optional 2 | request tracing, aimed at performance investigations. 3 | 4 | ## Enabling tracing 5 | 6 | Tracing is disabled by default. It can enabled with the following configuration 7 | line: 8 | 9 | tracing.enabled: true 10 | 11 | By default, JSON-encoded traces are written to `./traces-.json`. 12 | The following line sets a custom output file: 13 | 14 | tracing.outfile: /path/to/file.json 15 | 16 | -------------------------------------------------------------------------------- /docs/workflows.md: -------------------------------------------------------------------------------- 1 | # Workflows 2 | 3 | Serverledge accepts workflows defined by users through a subset of the JSON-based *Amazon States Language*, currently in use by AWS Step Functions. 4 | 5 | ## Tasks 6 | 7 | Serverledge workflows currently comprise 4 types of *tasks*: 8 | - **Simple**: a task that wraps a function. This is the only task that executes user-defined functions. 9 | - **Choice**: a task with N alternative branches, each associated with a condition; execution control and input data are transferred to the first branch whose condition is evaluated as true 10 | - **FanOut**: a task with N outputs that copies (or scatters) the input to all the outputs (with subsequent nodes activated in parallel); *experimental* 11 | - **FanIn**: a task with N inputs that waits for the termination of all the parent nodes, and then merges the results in one output. The node fails after a specified timeout. 12 | 13 | Other special types of tasks are always present and pre-built when using the APIs: 14 | - **Start**: the task from which the workflow starts executing (not associated with any function) 15 | - **End**: the final task of the workflow (not associated with any function) 16 | - **Success**: a task that -- as soon it is activated -- terminates workflow execution reporting success 17 | - **Fail**: a task that -- as soon it is activated -- terminates workflow execution reporting a failure 18 | 19 | 20 | ## Writing Functions 21 | 22 | *Simple* tasks execute a regular Serverledge function. Any function previously 23 | registered in Serverledge can be associated with a Simple task. The only 24 | additional requirement is that **functions used in workflows must be associated 25 | with a Signature upon creation** (which is optional in general). 26 | 27 | ### Signature 28 | A signature specifies the name and the type of the inputs accepted by the function, as well as the type of the returned outputs. For instance, a *Fibonacci* function might have a single integer input, and produce a single integer output. 29 | 30 | The signature can be specified when creating a function through the CLI. 31 | 32 | Inputs can be specified using the `--input` (or `-i` for short) option, while outputs are specified using the `--output` (or `-o` for short) option. 33 | 34 | Both inputs and outputs are specified using the syntax `name:type`, where `type` is one of the following strings: `Int`, `Float`, `Text`, `Bool`. 35 | 36 | Example: 37 | 38 | bin/serverledge-cli create --function inc \ 39 | --memory 128 \ 40 | --src examples/inc.py \ 41 | --runtime python310 \ 42 | --handler "inc.handler"\ 43 | --input "n:Int" --output "m:Int" 44 | 45 | 46 | 47 | ## Example 48 | 49 | The file `examples/workflow-simple.json` contains an example ASL definition of 50 | a workflow. To register it: 51 | 52 | bin/serverledge-cli create-workflow -s examples/workflow-simple.json -f myWorkflow 53 | 54 | To execute it with input `n=2`: 55 | 56 | bin/serverledge-cli invoke-workflow -f myWorkflow -p "input:2" 57 | 58 | -------------------------------------------------------------------------------- /docs/writing-functions.md: -------------------------------------------------------------------------------- 1 | # Writing functions 2 | 3 | Some languages have built-in support in Serverledge and is extremely easy to 4 | write new functions (e.g., Python, nodejs). 5 | For other languages, [custom container images](./custom_runtime.md) can be used to deploy and run 6 | functions. 7 | 8 | ## Python 9 | 10 | Available runtime: `python310` (Python 3.10) 11 | 12 | def handler_fun (context, params): 13 | return "..." 14 | 15 | Specify the handler as `.` (e.g., `myfile.handler_fun`). 16 | An example is given in `examples/hello.py`. 17 | 18 | ## NodeJS 19 | 20 | Available runtime: `nodejs17` (NodeJS 17) 21 | 22 | function handler_fun (context, params) { 23 | return "..." 24 | } 25 | 26 | module.exports = handler_fun // this is mandatory! 27 | 28 | Specify the handler as `.js` (e.g., `myfile.js`). 29 | An example is given in `examples/sieve.js`. 30 | 31 | ## Custom function runtimes 32 | 33 | Follow [these instructions](./custom_runtime.md). 34 | -------------------------------------------------------------------------------- /examples/c++/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM grussorusso/serverledge-base AS BASE 2 | FROM alpine:3.17.0 3 | 4 | RUN apk update && \ 5 | apk add --no-cache \ 6 | build-base 7 | 8 | WORKDIR /app 9 | 10 | COPY rapidjson/ ./rapidjson/ 11 | COPY function.cpp . 12 | COPY Makefile . 13 | 14 | RUN make 15 | 16 | WORKDIR / 17 | # Required: install the executor as /executor 18 | COPY --from=BASE /executor /executor 19 | CMD /executor 20 | 21 | ENV CUSTOM_CMD "/app/function" 22 | -------------------------------------------------------------------------------- /examples/c++/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | g++ -o function function.cpp 3 | -------------------------------------------------------------------------------- /examples/c++/README.md: -------------------------------------------------------------------------------- 1 | ## Running a C++ function 2 | 3 | In this example, we show how a C++ function can be deployed and executed in 4 | Serverledge. Compared to other languages (e.g., Python), more effort is required 5 | as Serverledge does not currently provide built-in support for C++. Therefore, 6 | we will need to build a custom container image for this purpose (more 7 | information can be found [here](../../docs/custom_runtime.md)). 8 | The image will contain (1) the Executor component of Serverledge and (2) the 9 | compiled C++ code of the function. 10 | 11 | The file `function.cpp` implements a simple function that takes 2 integer 12 | parameters `a` and `b`, and returns their sum. 13 | Most the code in the file is actually devoted to parsing the JSON-serialized 14 | parameters for the function, and encoding the returned JSON object. 15 | 16 | To build the image and register the function in Serverledge: 17 | 18 | docker build -t cpp-example . 19 | serverledge-cli create -function cpp -memory 256 -runtime custom -custom_image cpp-example 20 | 21 | To invoke the function: 22 | 23 | serverledge-cli invoke --function cpp --params_file example.json 24 | 25 | where `example.json` contains the input parameters: 26 | 27 | { 28 | "a":3, 29 | "b":42 30 | } 31 | 32 | -------------------------------------------------------------------------------- /examples/c++/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "a":3, 3 | "b":42 4 | } 5 | -------------------------------------------------------------------------------- /examples/c++/function.cpp: -------------------------------------------------------------------------------- 1 | #include "rapidjson/document.h" 2 | #include "rapidjson/filewritestream.h" 3 | #include "rapidjson/writer.h" 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | 10 | using namespace std; 11 | using namespace rapidjson; 12 | 13 | // This is the function code 14 | void fun (Document& params, Document& results) { 15 | if (!params.HasMember("a") || !params.HasMember("b")) 16 | return; 17 | if (!params["a"].IsInt() || !params["b"].IsInt()) 18 | return; 19 | int a = params["a"].GetInt(); 20 | int b = params["b"].GetInt(); 21 | 22 | // Add data to the JSON document with results 23 | results.AddMember("Sum", a+b, results.GetAllocator()); 24 | } 25 | 26 | 27 | int main() 28 | { 29 | // Open the input file 30 | ifstream file(std::getenv("PARAMS_FILE")); 31 | // Read the entire file into a string 32 | string json((istreambuf_iterator(file)), 33 | istreambuf_iterator()); 34 | 35 | // Create a Document object to hold the JSON data 36 | Document params; 37 | 38 | // Parse the JSON data 39 | params.Parse(json.c_str()); 40 | 41 | // Check for parse errors 42 | if (params.HasParseError()) { 43 | cerr << "Error parsing JSON: " << params.GetParseError() << endl; 44 | return 1; 45 | } 46 | 47 | 48 | Document d; 49 | d.SetObject(); 50 | 51 | fun(params, d); 52 | 53 | StringBuffer buffer; 54 | Writer writer(buffer); 55 | d.Accept(writer); 56 | 57 | // Open the output file 58 | std::ofstream outfile(std::getenv("RESULT_FILE")); 59 | // Output {"project":"rapidjson","stars":11} 60 | outfile << buffer.GetString() << std::endl; 61 | 62 | return 0; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/cursorstreamwrapper.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_CURSORSTREAMWRAPPER_H_ 16 | #define RAPIDJSON_CURSORSTREAMWRAPPER_H_ 17 | 18 | #include "stream.h" 19 | 20 | #if defined(__GNUC__) 21 | RAPIDJSON_DIAG_PUSH 22 | RAPIDJSON_DIAG_OFF(effc++) 23 | #endif 24 | 25 | #if defined(_MSC_VER) && _MSC_VER <= 1800 26 | RAPIDJSON_DIAG_PUSH 27 | RAPIDJSON_DIAG_OFF(4702) // unreachable code 28 | RAPIDJSON_DIAG_OFF(4512) // assignment operator could not be generated 29 | #endif 30 | 31 | RAPIDJSON_NAMESPACE_BEGIN 32 | 33 | 34 | //! Cursor stream wrapper for counting line and column number if error exists. 35 | /*! 36 | \tparam InputStream Any stream that implements Stream Concept 37 | */ 38 | template > 39 | class CursorStreamWrapper : public GenericStreamWrapper { 40 | public: 41 | typedef typename Encoding::Ch Ch; 42 | 43 | CursorStreamWrapper(InputStream& is): 44 | GenericStreamWrapper(is), line_(1), col_(0) {} 45 | 46 | // counting line and column number 47 | Ch Take() { 48 | Ch ch = this->is_.Take(); 49 | if(ch == '\n') { 50 | line_ ++; 51 | col_ = 0; 52 | } else { 53 | col_ ++; 54 | } 55 | return ch; 56 | } 57 | 58 | //! Get the error line number, if error exists. 59 | size_t GetLine() const { return line_; } 60 | //! Get the error column number, if error exists. 61 | size_t GetColumn() const { return col_; } 62 | 63 | private: 64 | size_t line_; //!< Current Line 65 | size_t col_; //!< Current Column 66 | }; 67 | 68 | #if defined(_MSC_VER) && _MSC_VER <= 1800 69 | RAPIDJSON_DIAG_POP 70 | #endif 71 | 72 | #if defined(__GNUC__) 73 | RAPIDJSON_DIAG_POP 74 | #endif 75 | 76 | RAPIDJSON_NAMESPACE_END 77 | 78 | #endif // RAPIDJSON_CURSORSTREAMWRAPPER_H_ 79 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/filereadstream.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_FILEREADSTREAM_H_ 16 | #define RAPIDJSON_FILEREADSTREAM_H_ 17 | 18 | #include "stream.h" 19 | #include 20 | 21 | #ifdef __clang__ 22 | RAPIDJSON_DIAG_PUSH 23 | RAPIDJSON_DIAG_OFF(padded) 24 | RAPIDJSON_DIAG_OFF(unreachable-code) 25 | RAPIDJSON_DIAG_OFF(missing-noreturn) 26 | #endif 27 | 28 | RAPIDJSON_NAMESPACE_BEGIN 29 | 30 | //! File byte stream for input using fread(). 31 | /*! 32 | \note implements Stream concept 33 | */ 34 | class FileReadStream { 35 | public: 36 | typedef char Ch; //!< Character type (byte). 37 | 38 | //! Constructor. 39 | /*! 40 | \param fp File pointer opened for read. 41 | \param buffer user-supplied buffer. 42 | \param bufferSize size of buffer in bytes. Must >=4 bytes. 43 | */ 44 | FileReadStream(std::FILE* fp, char* buffer, size_t bufferSize) : fp_(fp), buffer_(buffer), bufferSize_(bufferSize), bufferLast_(0), current_(buffer_), readCount_(0), count_(0), eof_(false) { 45 | RAPIDJSON_ASSERT(fp_ != 0); 46 | RAPIDJSON_ASSERT(bufferSize >= 4); 47 | Read(); 48 | } 49 | 50 | Ch Peek() const { return *current_; } 51 | Ch Take() { Ch c = *current_; Read(); return c; } 52 | size_t Tell() const { return count_ + static_cast(current_ - buffer_); } 53 | 54 | // Not implemented 55 | void Put(Ch) { RAPIDJSON_ASSERT(false); } 56 | void Flush() { RAPIDJSON_ASSERT(false); } 57 | Ch* PutBegin() { RAPIDJSON_ASSERT(false); return 0; } 58 | size_t PutEnd(Ch*) { RAPIDJSON_ASSERT(false); return 0; } 59 | 60 | // For encoding detection only. 61 | const Ch* Peek4() const { 62 | return (current_ + 4 - !eof_ <= bufferLast_) ? current_ : 0; 63 | } 64 | 65 | private: 66 | void Read() { 67 | if (current_ < bufferLast_) 68 | ++current_; 69 | else if (!eof_) { 70 | count_ += readCount_; 71 | readCount_ = std::fread(buffer_, 1, bufferSize_, fp_); 72 | bufferLast_ = buffer_ + readCount_ - 1; 73 | current_ = buffer_; 74 | 75 | if (readCount_ < bufferSize_) { 76 | buffer_[readCount_] = '\0'; 77 | ++bufferLast_; 78 | eof_ = true; 79 | } 80 | } 81 | } 82 | 83 | std::FILE* fp_; 84 | Ch *buffer_; 85 | size_t bufferSize_; 86 | Ch *bufferLast_; 87 | Ch *current_; 88 | size_t readCount_; 89 | size_t count_; //!< Number of characters read 90 | bool eof_; 91 | }; 92 | 93 | RAPIDJSON_NAMESPACE_END 94 | 95 | #ifdef __clang__ 96 | RAPIDJSON_DIAG_POP 97 | #endif 98 | 99 | #endif // RAPIDJSON_FILESTREAM_H_ 100 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/filewritestream.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_FILEWRITESTREAM_H_ 16 | #define RAPIDJSON_FILEWRITESTREAM_H_ 17 | 18 | #include "stream.h" 19 | #include 20 | 21 | #ifdef __clang__ 22 | RAPIDJSON_DIAG_PUSH 23 | RAPIDJSON_DIAG_OFF(unreachable-code) 24 | #endif 25 | 26 | RAPIDJSON_NAMESPACE_BEGIN 27 | 28 | //! Wrapper of C file stream for output using fwrite(). 29 | /*! 30 | \note implements Stream concept 31 | */ 32 | class FileWriteStream { 33 | public: 34 | typedef char Ch; //!< Character type. Only support char. 35 | 36 | FileWriteStream(std::FILE* fp, char* buffer, size_t bufferSize) : fp_(fp), buffer_(buffer), bufferEnd_(buffer + bufferSize), current_(buffer_) { 37 | RAPIDJSON_ASSERT(fp_ != 0); 38 | } 39 | 40 | void Put(char c) { 41 | if (current_ >= bufferEnd_) 42 | Flush(); 43 | 44 | *current_++ = c; 45 | } 46 | 47 | void PutN(char c, size_t n) { 48 | size_t avail = static_cast(bufferEnd_ - current_); 49 | while (n > avail) { 50 | std::memset(current_, c, avail); 51 | current_ += avail; 52 | Flush(); 53 | n -= avail; 54 | avail = static_cast(bufferEnd_ - current_); 55 | } 56 | 57 | if (n > 0) { 58 | std::memset(current_, c, n); 59 | current_ += n; 60 | } 61 | } 62 | 63 | void Flush() { 64 | if (current_ != buffer_) { 65 | size_t result = std::fwrite(buffer_, 1, static_cast(current_ - buffer_), fp_); 66 | if (result < static_cast(current_ - buffer_)) { 67 | // failure deliberately ignored at this time 68 | // added to avoid warn_unused_result build errors 69 | } 70 | current_ = buffer_; 71 | } 72 | } 73 | 74 | // Not implemented 75 | char Peek() const { RAPIDJSON_ASSERT(false); return 0; } 76 | char Take() { RAPIDJSON_ASSERT(false); return 0; } 77 | size_t Tell() const { RAPIDJSON_ASSERT(false); return 0; } 78 | char* PutBegin() { RAPIDJSON_ASSERT(false); return 0; } 79 | size_t PutEnd(char*) { RAPIDJSON_ASSERT(false); return 0; } 80 | 81 | private: 82 | // Prohibit copy constructor & assignment operator. 83 | FileWriteStream(const FileWriteStream&); 84 | FileWriteStream& operator=(const FileWriteStream&); 85 | 86 | std::FILE* fp_; 87 | char *buffer_; 88 | char *bufferEnd_; 89 | char *current_; 90 | }; 91 | 92 | //! Implement specialized version of PutN() with memset() for better performance. 93 | template<> 94 | inline void PutN(FileWriteStream& stream, char c, size_t n) { 95 | stream.PutN(c, n); 96 | } 97 | 98 | RAPIDJSON_NAMESPACE_END 99 | 100 | #ifdef __clang__ 101 | RAPIDJSON_DIAG_POP 102 | #endif 103 | 104 | #endif // RAPIDJSON_FILESTREAM_H_ 105 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/internal/clzll.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_CLZLL_H_ 16 | #define RAPIDJSON_CLZLL_H_ 17 | 18 | #include "../rapidjson.h" 19 | 20 | #if defined(_MSC_VER) && !defined(UNDER_CE) 21 | #include 22 | #if defined(_WIN64) 23 | #pragma intrinsic(_BitScanReverse64) 24 | #else 25 | #pragma intrinsic(_BitScanReverse) 26 | #endif 27 | #endif 28 | 29 | RAPIDJSON_NAMESPACE_BEGIN 30 | namespace internal { 31 | 32 | inline uint32_t clzll(uint64_t x) { 33 | // Passing 0 to __builtin_clzll is UB in GCC and results in an 34 | // infinite loop in the software implementation. 35 | RAPIDJSON_ASSERT(x != 0); 36 | 37 | #if defined(_MSC_VER) && !defined(UNDER_CE) 38 | unsigned long r = 0; 39 | #if defined(_WIN64) 40 | _BitScanReverse64(&r, x); 41 | #else 42 | // Scan the high 32 bits. 43 | if (_BitScanReverse(&r, static_cast(x >> 32))) 44 | return 63 - (r + 32); 45 | 46 | // Scan the low 32 bits. 47 | _BitScanReverse(&r, static_cast(x & 0xFFFFFFFF)); 48 | #endif // _WIN64 49 | 50 | return 63 - r; 51 | #elif (defined(__GNUC__) && __GNUC__ >= 4) || RAPIDJSON_HAS_BUILTIN(__builtin_clzll) 52 | // __builtin_clzll wrapper 53 | return static_cast(__builtin_clzll(x)); 54 | #else 55 | // naive version 56 | uint32_t r = 0; 57 | while (!(x & (static_cast(1) << 63))) { 58 | x <<= 1; 59 | ++r; 60 | } 61 | 62 | return r; 63 | #endif // _MSC_VER 64 | } 65 | 66 | #define RAPIDJSON_CLZLL RAPIDJSON_NAMESPACE::internal::clzll 67 | 68 | } // namespace internal 69 | RAPIDJSON_NAMESPACE_END 70 | 71 | #endif // RAPIDJSON_CLZLL_H_ 72 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/internal/ieee754.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_IEEE754_ 16 | #define RAPIDJSON_IEEE754_ 17 | 18 | #include "../rapidjson.h" 19 | 20 | RAPIDJSON_NAMESPACE_BEGIN 21 | namespace internal { 22 | 23 | class Double { 24 | public: 25 | Double() {} 26 | Double(double d) : d_(d) {} 27 | Double(uint64_t u) : u_(u) {} 28 | 29 | double Value() const { return d_; } 30 | uint64_t Uint64Value() const { return u_; } 31 | 32 | double NextPositiveDouble() const { 33 | RAPIDJSON_ASSERT(!Sign()); 34 | return Double(u_ + 1).Value(); 35 | } 36 | 37 | bool Sign() const { return (u_ & kSignMask) != 0; } 38 | uint64_t Significand() const { return u_ & kSignificandMask; } 39 | int Exponent() const { return static_cast(((u_ & kExponentMask) >> kSignificandSize) - kExponentBias); } 40 | 41 | bool IsNan() const { return (u_ & kExponentMask) == kExponentMask && Significand() != 0; } 42 | bool IsInf() const { return (u_ & kExponentMask) == kExponentMask && Significand() == 0; } 43 | bool IsNanOrInf() const { return (u_ & kExponentMask) == kExponentMask; } 44 | bool IsNormal() const { return (u_ & kExponentMask) != 0 || Significand() == 0; } 45 | bool IsZero() const { return (u_ & (kExponentMask | kSignificandMask)) == 0; } 46 | 47 | uint64_t IntegerSignificand() const { return IsNormal() ? Significand() | kHiddenBit : Significand(); } 48 | int IntegerExponent() const { return (IsNormal() ? Exponent() : kDenormalExponent) - kSignificandSize; } 49 | uint64_t ToBias() const { return (u_ & kSignMask) ? ~u_ + 1 : u_ | kSignMask; } 50 | 51 | static int EffectiveSignificandSize(int order) { 52 | if (order >= -1021) 53 | return 53; 54 | else if (order <= -1074) 55 | return 0; 56 | else 57 | return order + 1074; 58 | } 59 | 60 | private: 61 | static const int kSignificandSize = 52; 62 | static const int kExponentBias = 0x3FF; 63 | static const int kDenormalExponent = 1 - kExponentBias; 64 | static const uint64_t kSignMask = RAPIDJSON_UINT64_C2(0x80000000, 0x00000000); 65 | static const uint64_t kExponentMask = RAPIDJSON_UINT64_C2(0x7FF00000, 0x00000000); 66 | static const uint64_t kSignificandMask = RAPIDJSON_UINT64_C2(0x000FFFFF, 0xFFFFFFFF); 67 | static const uint64_t kHiddenBit = RAPIDJSON_UINT64_C2(0x00100000, 0x00000000); 68 | 69 | union { 70 | double d_; 71 | uint64_t u_; 72 | }; 73 | }; 74 | 75 | } // namespace internal 76 | RAPIDJSON_NAMESPACE_END 77 | 78 | #endif // RAPIDJSON_IEEE754_ 79 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/internal/pow10.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_POW10_ 16 | #define RAPIDJSON_POW10_ 17 | 18 | #include "../rapidjson.h" 19 | 20 | RAPIDJSON_NAMESPACE_BEGIN 21 | namespace internal { 22 | 23 | //! Computes integer powers of 10 in double (10.0^n). 24 | /*! This function uses lookup table for fast and accurate results. 25 | \param n non-negative exponent. Must <= 308. 26 | \return 10.0^n 27 | */ 28 | inline double Pow10(int n) { 29 | static const double e[] = { // 1e-0...1e308: 309 * 8 bytes = 2472 bytes 30 | 1e+0, 31 | 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20, 32 | 1e+21, 1e+22, 1e+23, 1e+24, 1e+25, 1e+26, 1e+27, 1e+28, 1e+29, 1e+30, 1e+31, 1e+32, 1e+33, 1e+34, 1e+35, 1e+36, 1e+37, 1e+38, 1e+39, 1e+40, 33 | 1e+41, 1e+42, 1e+43, 1e+44, 1e+45, 1e+46, 1e+47, 1e+48, 1e+49, 1e+50, 1e+51, 1e+52, 1e+53, 1e+54, 1e+55, 1e+56, 1e+57, 1e+58, 1e+59, 1e+60, 34 | 1e+61, 1e+62, 1e+63, 1e+64, 1e+65, 1e+66, 1e+67, 1e+68, 1e+69, 1e+70, 1e+71, 1e+72, 1e+73, 1e+74, 1e+75, 1e+76, 1e+77, 1e+78, 1e+79, 1e+80, 35 | 1e+81, 1e+82, 1e+83, 1e+84, 1e+85, 1e+86, 1e+87, 1e+88, 1e+89, 1e+90, 1e+91, 1e+92, 1e+93, 1e+94, 1e+95, 1e+96, 1e+97, 1e+98, 1e+99, 1e+100, 36 | 1e+101,1e+102,1e+103,1e+104,1e+105,1e+106,1e+107,1e+108,1e+109,1e+110,1e+111,1e+112,1e+113,1e+114,1e+115,1e+116,1e+117,1e+118,1e+119,1e+120, 37 | 1e+121,1e+122,1e+123,1e+124,1e+125,1e+126,1e+127,1e+128,1e+129,1e+130,1e+131,1e+132,1e+133,1e+134,1e+135,1e+136,1e+137,1e+138,1e+139,1e+140, 38 | 1e+141,1e+142,1e+143,1e+144,1e+145,1e+146,1e+147,1e+148,1e+149,1e+150,1e+151,1e+152,1e+153,1e+154,1e+155,1e+156,1e+157,1e+158,1e+159,1e+160, 39 | 1e+161,1e+162,1e+163,1e+164,1e+165,1e+166,1e+167,1e+168,1e+169,1e+170,1e+171,1e+172,1e+173,1e+174,1e+175,1e+176,1e+177,1e+178,1e+179,1e+180, 40 | 1e+181,1e+182,1e+183,1e+184,1e+185,1e+186,1e+187,1e+188,1e+189,1e+190,1e+191,1e+192,1e+193,1e+194,1e+195,1e+196,1e+197,1e+198,1e+199,1e+200, 41 | 1e+201,1e+202,1e+203,1e+204,1e+205,1e+206,1e+207,1e+208,1e+209,1e+210,1e+211,1e+212,1e+213,1e+214,1e+215,1e+216,1e+217,1e+218,1e+219,1e+220, 42 | 1e+221,1e+222,1e+223,1e+224,1e+225,1e+226,1e+227,1e+228,1e+229,1e+230,1e+231,1e+232,1e+233,1e+234,1e+235,1e+236,1e+237,1e+238,1e+239,1e+240, 43 | 1e+241,1e+242,1e+243,1e+244,1e+245,1e+246,1e+247,1e+248,1e+249,1e+250,1e+251,1e+252,1e+253,1e+254,1e+255,1e+256,1e+257,1e+258,1e+259,1e+260, 44 | 1e+261,1e+262,1e+263,1e+264,1e+265,1e+266,1e+267,1e+268,1e+269,1e+270,1e+271,1e+272,1e+273,1e+274,1e+275,1e+276,1e+277,1e+278,1e+279,1e+280, 45 | 1e+281,1e+282,1e+283,1e+284,1e+285,1e+286,1e+287,1e+288,1e+289,1e+290,1e+291,1e+292,1e+293,1e+294,1e+295,1e+296,1e+297,1e+298,1e+299,1e+300, 46 | 1e+301,1e+302,1e+303,1e+304,1e+305,1e+306,1e+307,1e+308 47 | }; 48 | RAPIDJSON_ASSERT(n >= 0 && n <= 308); 49 | return e[n]; 50 | } 51 | 52 | } // namespace internal 53 | RAPIDJSON_NAMESPACE_END 54 | 55 | #endif // RAPIDJSON_POW10_ 56 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/internal/strfunc.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_INTERNAL_STRFUNC_H_ 16 | #define RAPIDJSON_INTERNAL_STRFUNC_H_ 17 | 18 | #include "../stream.h" 19 | #include 20 | 21 | RAPIDJSON_NAMESPACE_BEGIN 22 | namespace internal { 23 | 24 | //! Custom strlen() which works on different character types. 25 | /*! \tparam Ch Character type (e.g. char, wchar_t, short) 26 | \param s Null-terminated input string. 27 | \return Number of characters in the string. 28 | \note This has the same semantics as strlen(), the return value is not number of Unicode codepoints. 29 | */ 30 | template 31 | inline SizeType StrLen(const Ch* s) { 32 | RAPIDJSON_ASSERT(s != 0); 33 | const Ch* p = s; 34 | while (*p) ++p; 35 | return SizeType(p - s); 36 | } 37 | 38 | template <> 39 | inline SizeType StrLen(const char* s) { 40 | return SizeType(std::strlen(s)); 41 | } 42 | 43 | template <> 44 | inline SizeType StrLen(const wchar_t* s) { 45 | return SizeType(std::wcslen(s)); 46 | } 47 | 48 | //! Custom strcmpn() which works on different character types. 49 | /*! \tparam Ch Character type (e.g. char, wchar_t, short) 50 | \param s1 Null-terminated input string. 51 | \param s2 Null-terminated input string. 52 | \return 0 if equal 53 | */ 54 | template 55 | inline int StrCmp(const Ch* s1, const Ch* s2) { 56 | RAPIDJSON_ASSERT(s1 != 0); 57 | RAPIDJSON_ASSERT(s2 != 0); 58 | while(*s1 && (*s1 == *s2)) { s1++; s2++; } 59 | return static_cast(*s1) < static_cast(*s2) ? -1 : static_cast(*s1) > static_cast(*s2); 60 | } 61 | 62 | //! Returns number of code points in a encoded string. 63 | template 64 | bool CountStringCodePoint(const typename Encoding::Ch* s, SizeType length, SizeType* outCount) { 65 | RAPIDJSON_ASSERT(s != 0); 66 | RAPIDJSON_ASSERT(outCount != 0); 67 | GenericStringStream is(s); 68 | const typename Encoding::Ch* end = s + length; 69 | SizeType count = 0; 70 | while (is.src_ < end) { 71 | unsigned codepoint; 72 | if (!Encoding::Decode(is, &codepoint)) 73 | return false; 74 | count++; 75 | } 76 | *outCount = count; 77 | return true; 78 | } 79 | 80 | } // namespace internal 81 | RAPIDJSON_NAMESPACE_END 82 | 83 | #endif // RAPIDJSON_INTERNAL_STRFUNC_H_ 84 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/internal/swap.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_INTERNAL_SWAP_H_ 16 | #define RAPIDJSON_INTERNAL_SWAP_H_ 17 | 18 | #include "../rapidjson.h" 19 | 20 | #if defined(__clang__) 21 | RAPIDJSON_DIAG_PUSH 22 | RAPIDJSON_DIAG_OFF(c++98-compat) 23 | #endif 24 | 25 | RAPIDJSON_NAMESPACE_BEGIN 26 | namespace internal { 27 | 28 | //! Custom swap() to avoid dependency on C++ header 29 | /*! \tparam T Type of the arguments to swap, should be instantiated with primitive C++ types only. 30 | \note This has the same semantics as std::swap(). 31 | */ 32 | template 33 | inline void Swap(T& a, T& b) RAPIDJSON_NOEXCEPT { 34 | T tmp = a; 35 | a = b; 36 | b = tmp; 37 | } 38 | 39 | } // namespace internal 40 | RAPIDJSON_NAMESPACE_END 41 | 42 | #if defined(__clang__) 43 | RAPIDJSON_DIAG_POP 44 | #endif 45 | 46 | #endif // RAPIDJSON_INTERNAL_SWAP_H_ 47 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/memorybuffer.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_MEMORYBUFFER_H_ 16 | #define RAPIDJSON_MEMORYBUFFER_H_ 17 | 18 | #include "stream.h" 19 | #include "internal/stack.h" 20 | 21 | RAPIDJSON_NAMESPACE_BEGIN 22 | 23 | //! Represents an in-memory output byte stream. 24 | /*! 25 | This class is mainly for being wrapped by EncodedOutputStream or AutoUTFOutputStream. 26 | 27 | It is similar to FileWriteBuffer but the destination is an in-memory buffer instead of a file. 28 | 29 | Differences between MemoryBuffer and StringBuffer: 30 | 1. StringBuffer has Encoding but MemoryBuffer is only a byte buffer. 31 | 2. StringBuffer::GetString() returns a null-terminated string. MemoryBuffer::GetBuffer() returns a buffer without terminator. 32 | 33 | \tparam Allocator type for allocating memory buffer. 34 | \note implements Stream concept 35 | */ 36 | template 37 | struct GenericMemoryBuffer { 38 | typedef char Ch; // byte 39 | 40 | GenericMemoryBuffer(Allocator* allocator = 0, size_t capacity = kDefaultCapacity) : stack_(allocator, capacity) {} 41 | 42 | void Put(Ch c) { *stack_.template Push() = c; } 43 | void Flush() {} 44 | 45 | void Clear() { stack_.Clear(); } 46 | void ShrinkToFit() { stack_.ShrinkToFit(); } 47 | Ch* Push(size_t count) { return stack_.template Push(count); } 48 | void Pop(size_t count) { stack_.template Pop(count); } 49 | 50 | const Ch* GetBuffer() const { 51 | return stack_.template Bottom(); 52 | } 53 | 54 | size_t GetSize() const { return stack_.GetSize(); } 55 | 56 | static const size_t kDefaultCapacity = 256; 57 | mutable internal::Stack stack_; 58 | }; 59 | 60 | typedef GenericMemoryBuffer<> MemoryBuffer; 61 | 62 | //! Implement specialized version of PutN() with memset() for better performance. 63 | template<> 64 | inline void PutN(MemoryBuffer& memoryBuffer, char c, size_t n) { 65 | std::memset(memoryBuffer.stack_.Push(n), c, n * sizeof(c)); 66 | } 67 | 68 | RAPIDJSON_NAMESPACE_END 69 | 70 | #endif // RAPIDJSON_MEMORYBUFFER_H_ 71 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/memorystream.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_MEMORYSTREAM_H_ 16 | #define RAPIDJSON_MEMORYSTREAM_H_ 17 | 18 | #include "stream.h" 19 | 20 | #ifdef __clang__ 21 | RAPIDJSON_DIAG_PUSH 22 | RAPIDJSON_DIAG_OFF(unreachable-code) 23 | RAPIDJSON_DIAG_OFF(missing-noreturn) 24 | #endif 25 | 26 | RAPIDJSON_NAMESPACE_BEGIN 27 | 28 | //! Represents an in-memory input byte stream. 29 | /*! 30 | This class is mainly for being wrapped by EncodedInputStream or AutoUTFInputStream. 31 | 32 | It is similar to FileReadBuffer but the source is an in-memory buffer instead of a file. 33 | 34 | Differences between MemoryStream and StringStream: 35 | 1. StringStream has encoding but MemoryStream is a byte stream. 36 | 2. MemoryStream needs size of the source buffer and the buffer don't need to be null terminated. StringStream assume null-terminated string as source. 37 | 3. MemoryStream supports Peek4() for encoding detection. StringStream is specified with an encoding so it should not have Peek4(). 38 | \note implements Stream concept 39 | */ 40 | struct MemoryStream { 41 | typedef char Ch; // byte 42 | 43 | MemoryStream(const Ch *src, size_t size) : src_(src), begin_(src), end_(src + size), size_(size) {} 44 | 45 | Ch Peek() const { return RAPIDJSON_UNLIKELY(src_ == end_) ? '\0' : *src_; } 46 | Ch Take() { return RAPIDJSON_UNLIKELY(src_ == end_) ? '\0' : *src_++; } 47 | size_t Tell() const { return static_cast(src_ - begin_); } 48 | 49 | Ch* PutBegin() { RAPIDJSON_ASSERT(false); return 0; } 50 | void Put(Ch) { RAPIDJSON_ASSERT(false); } 51 | void Flush() { RAPIDJSON_ASSERT(false); } 52 | size_t PutEnd(Ch*) { RAPIDJSON_ASSERT(false); return 0; } 53 | 54 | // For encoding detection only. 55 | const Ch* Peek4() const { 56 | return Tell() + 4 <= size_ ? src_ : 0; 57 | } 58 | 59 | const Ch* src_; //!< Current read position. 60 | const Ch* begin_; //!< Original head of the string. 61 | const Ch* end_; //!< End of stream. 62 | size_t size_; //!< Size of the stream. 63 | }; 64 | 65 | RAPIDJSON_NAMESPACE_END 66 | 67 | #ifdef __clang__ 68 | RAPIDJSON_DIAG_POP 69 | #endif 70 | 71 | #endif // RAPIDJSON_MEMORYBUFFER_H_ 72 | -------------------------------------------------------------------------------- /examples/c++/rapidjson/ostreamwrapper.h: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making RapidJSON available. 2 | // 3 | // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. 4 | // 5 | // Licensed under the MIT License (the "License"); you may not use this file except 6 | // in compliance with the License. You may obtain a copy of the License at 7 | // 8 | // http://opensource.org/licenses/MIT 9 | // 10 | // Unless required by applicable law or agreed to in writing, software distributed 11 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | // specific language governing permissions and limitations under the License. 14 | 15 | #ifndef RAPIDJSON_OSTREAMWRAPPER_H_ 16 | #define RAPIDJSON_OSTREAMWRAPPER_H_ 17 | 18 | #include "stream.h" 19 | #include 20 | 21 | #ifdef __clang__ 22 | RAPIDJSON_DIAG_PUSH 23 | RAPIDJSON_DIAG_OFF(padded) 24 | #endif 25 | 26 | RAPIDJSON_NAMESPACE_BEGIN 27 | 28 | //! Wrapper of \c std::basic_ostream into RapidJSON's Stream concept. 29 | /*! 30 | The classes can be wrapped including but not limited to: 31 | 32 | - \c std::ostringstream 33 | - \c std::stringstream 34 | - \c std::wpstringstream 35 | - \c std::wstringstream 36 | - \c std::ifstream 37 | - \c std::fstream 38 | - \c std::wofstream 39 | - \c std::wfstream 40 | 41 | \tparam StreamType Class derived from \c std::basic_ostream. 42 | */ 43 | 44 | template 45 | class BasicOStreamWrapper { 46 | public: 47 | typedef typename StreamType::char_type Ch; 48 | BasicOStreamWrapper(StreamType& stream) : stream_(stream) {} 49 | 50 | void Put(Ch c) { 51 | stream_.put(c); 52 | } 53 | 54 | void Flush() { 55 | stream_.flush(); 56 | } 57 | 58 | // Not implemented 59 | char Peek() const { RAPIDJSON_ASSERT(false); return 0; } 60 | char Take() { RAPIDJSON_ASSERT(false); return 0; } 61 | size_t Tell() const { RAPIDJSON_ASSERT(false); return 0; } 62 | char* PutBegin() { RAPIDJSON_ASSERT(false); return 0; } 63 | size_t PutEnd(char*) { RAPIDJSON_ASSERT(false); return 0; } 64 | 65 | private: 66 | BasicOStreamWrapper(const BasicOStreamWrapper&); 67 | BasicOStreamWrapper& operator=(const BasicOStreamWrapper&); 68 | 69 | StreamType& stream_; 70 | }; 71 | 72 | typedef BasicOStreamWrapper OStreamWrapper; 73 | typedef BasicOStreamWrapper WOStreamWrapper; 74 | 75 | #ifdef __clang__ 76 | RAPIDJSON_DIAG_POP 77 | #endif 78 | 79 | RAPIDJSON_NAMESPACE_END 80 | 81 | #endif // RAPIDJSON_OSTREAMWRAPPER_H_ 82 | -------------------------------------------------------------------------------- /examples/classes_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "class2", 4 | "utility": 0, 5 | "MaximumResponseTime": -1 6 | }, 7 | { 8 | "name": "class1", 9 | "utility": 1, 10 | "MaximumResponseTime": 20 11 | }, 12 | { 13 | "name": "class3", 14 | "utility": 0.5, 15 | "MaximumResponseTime": 2, 16 | "CompletedPercentage": 0.9 17 | } 18 | ] -------------------------------------------------------------------------------- /examples/config_edgeCloud.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | pool: 3 | cpus: 4 4 | memory: 1024 5 | api: 6 | port: 1323 7 | cloud: 8 | server: 9 | url: http://192.168.1.16:1326 10 | scheduler: 11 | policy: edgecloud -------------------------------------------------------------------------------- /examples/config_example.yaml: -------------------------------------------------------------------------------- 1 | metrics: 2 | enabled: true 3 | container: 4 | pool: 5 | cpus: 2 6 | memory: 1024 7 | api: 8 | port: 1323 9 | cloud: 10 | server: 11 | url: http://192.168.1.51:1325 12 | scheduler: 13 | #policy: edgecloud 14 | policy: customCloudOffload 15 | version: flux 16 | #policy: customCloudOffloadPrometheus 17 | cloud: 18 | cost: 0.0001 19 | registry: 20 | udp: 21 | port: 9876 22 | solver: 23 | address: "localhost:2500" 24 | storage: 25 | address: "http://localhost:8086" 26 | token: "serverledge" 27 | orgname: "serverledge" -------------------------------------------------------------------------------- /examples/config_example_2.yaml: -------------------------------------------------------------------------------- 1 | metrics: 2 | enabled: true 3 | container: 4 | pool: 5 | cpus: 2 6 | memory: 1024 7 | api: 8 | port: 1324 9 | cloud: 10 | server: 11 | url: http://192.168.1.21:1325 12 | scheduler: 13 | policy: customCloudOffload 14 | registry: 15 | udp: 16 | port: 9877 -------------------------------------------------------------------------------- /examples/config_example_cloud.yaml: -------------------------------------------------------------------------------- 1 | metrics: 2 | enabled: true 3 | port: 2113 4 | container: 5 | pool: 6 | cpus: 4 7 | memory: 20000 8 | api: 9 | port: 1325 10 | cloud: true 11 | registry: 12 | udp: 13 | port: 9878 -------------------------------------------------------------------------------- /examples/custom_hello/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM grussorusso/serverledge-base as BASE 2 | 3 | # Extend any image you want, e.g.; 4 | FROM python:3.8.1 5 | 6 | # Required: install the executor as /executor 7 | COPY --from=BASE /executor / 8 | CMD /executor 9 | 10 | # Required: this is the command representing your function 11 | ENV CUSTOM_CMD "python /function.py" 12 | 13 | # Install your code and any dependency, e.g.: 14 | COPY function.py / 15 | -------------------------------------------------------------------------------- /examples/custom_hello/function.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | # Set by Executor 5 | result_file = os.environ["RESULT_FILE"] 6 | params_file = os.environ["PARAMS_FILE"] 7 | params = {} 8 | if params_file != "": 9 | with open(params_file, "rb") as fp: 10 | params = json.load(fp) 11 | result = {} 12 | 13 | 14 | with open(result_file, "w") as outf: 15 | result["Params"] = params 16 | result["Message"] = "Hello!" 17 | 18 | outf.write(json.dumps(result)) 19 | 20 | -------------------------------------------------------------------------------- /examples/double.py: -------------------------------------------------------------------------------- 1 | def handler(params, context): 2 | return int(params["input"]) * 2 3 | -------------------------------------------------------------------------------- /examples/fibonacci.py: -------------------------------------------------------------------------------- 1 | def handler(params, context): 2 | n = params["n"] 3 | return ''.join(fibonacci_nums(int(n))) 4 | 5 | 6 | def fibonacci_nums(n): 7 | sequence = "" 8 | if n <= 0: 9 | sequence += "0" 10 | return sequence 11 | sequence = "0, 1" 12 | count = 2 13 | n1 = 0 14 | n2 = 1 15 | while count <= n: 16 | next_value = n2 + n1 17 | sequence += "," + "".join(str(next_value)) 18 | n1 = n2 19 | n2 = next_value 20 | count += 1 21 | return sequence 22 | -------------------------------------------------------------------------------- /examples/fibonacciNout.py: -------------------------------------------------------------------------------- 1 | def handler(params, context): 2 | n = params["n"] 3 | return ''.join(fibonacci_nums(int(n))) 4 | 5 | 6 | def fibonacci_nums(n): 7 | sequence = "" 8 | if n <= 0: 9 | sequence += "0" 10 | return sequence 11 | sequence = "0, 1" 12 | count = 2 13 | n1 = 0 14 | n2 = 1 15 | while count <= n: 16 | next_value = n2 + n1 17 | sequence += "," + "".join(str(next_value)) 18 | n1 = n2 19 | n2 = next_value 20 | count += 1 21 | return "Done" 22 | -------------------------------------------------------------------------------- /examples/grep.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def handler(params, context): 5 | return grep("grep", params["InputText"]) 6 | 7 | 8 | def grep(pattern, text): 9 | lines = text.split('\n') 10 | result = [line for line in lines if re.search(pattern, line)] 11 | return '\n'.join(result) 12 | -------------------------------------------------------------------------------- /examples/grepInput.json: -------------------------------------------------------------------------------- 1 | { 2 | "InputText": [ 3 | "This is an example text for testing the grep function.\nYou can use the grep function to search for specific words or patterns in text.\nThe function is a powerful tool for text processing.\n", 4 | "It allows you to filter and extract lines that match a given pattern.\nYou can customize the pattern using regular expressions.\nFeel free to test the grep function with different patterns and texts." 5 | ], 6 | "Pattern": "grep" 7 | } -------------------------------------------------------------------------------- /examples/hash_string.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def handler(params, context): 5 | n = params["n"] 6 | return ''.join(hash_string(n)) 7 | 8 | 9 | # Hash string 10 | def hash_string(s): 11 | return hashlib.sha256(s.encode()).hexdigest() 12 | -------------------------------------------------------------------------------- /examples/hello.js: -------------------------------------------------------------------------------- 1 | function handler(params, context) { 2 | return "Hello from JS!" 3 | } 4 | 5 | module.exports = handler 6 | -------------------------------------------------------------------------------- /examples/hello.py: -------------------------------------------------------------------------------- 1 | def handler(params, context): 2 | print("Executing function....") 3 | return "Hello, Serverledge!\nParams: {}".format(params) 4 | -------------------------------------------------------------------------------- /examples/inc.js: -------------------------------------------------------------------------------- 1 | function handler(params, context) { 2 | console.log(params); 3 | console.log("" + params["input"]); 4 | return parseInt(params["input"], 10) + 1 5 | } 6 | 7 | module.exports = handler; -------------------------------------------------------------------------------- /examples/inc.py: -------------------------------------------------------------------------------- 1 | def handler(params, context): 2 | print(f"Invoked inc with input: {params}") 3 | return int(params["input"]) + 1 4 | -------------------------------------------------------------------------------- /examples/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "n": "3" 3 | } -------------------------------------------------------------------------------- /examples/input2.json: -------------------------------------------------------------------------------- 1 | { 2 | "imgurl":"https://cdn.pixabay.com/photo/2016/02/19/15/46/labrador-retriever-1210559__340.jpg" 3 | } 4 | -------------------------------------------------------------------------------- /examples/isprime.py: -------------------------------------------------------------------------------- 1 | def handler(params, context): 2 | try: 3 | n = int(params["n"]) 4 | print(f"Checking n = {n}") 5 | result = is_prime(n) 6 | return {"IsPrime": result} 7 | except: 8 | return {} 9 | 10 | 11 | def is_prime(n): 12 | for i in range(2, n//2): 13 | if n%i == 0: 14 | return False 15 | return True 16 | 17 | -------------------------------------------------------------------------------- /examples/isprimeWithNumber.py: -------------------------------------------------------------------------------- 1 | def handler(params, context): 2 | try: 3 | n = int(params["n"]) 4 | result = is_prime(n) 5 | return {"IsPrime": result, "n": n} 6 | except: 7 | return {} 8 | 9 | 10 | def is_prime(n): 11 | for i in range(2, n//2): 12 | if n%i == 0: 13 | return False 14 | return True 15 | 16 | -------------------------------------------------------------------------------- /examples/jsonschema/Dockerfile: -------------------------------------------------------------------------------- 1 | #FROM grussorusso/serverledge-python310 2 | FROM python:3.10-alpine3.16 3 | RUN pip3 install jsonschema 4 | 5 | 6 | COPY executor.py / 7 | COPY function.py / 8 | 9 | WORKDIR / 10 | CMD python executor.py 11 | -------------------------------------------------------------------------------- /examples/jsonschema/README.md: -------------------------------------------------------------------------------- 1 | This examples demonstrates how to define a function through a custom 2 | container image. In particular, we define a Python function that validates 3 | user-given JSON input according to a fixed schema. For this purpose, we use 4 | the `jsonschema` Python library, which is not available in the default Python 5 | runtime image. 6 | 7 | The actual function code is in `function.py`. We also need to copy an Executor 8 | implementation (see the docs) to the container. The file `executor.py` contains 9 | an adapted version of the Executor implementation taken from the default Python 10 | runtime image of Serverledge. 11 | 12 | ## Building the image 13 | 14 | $ docker build -t . 15 | 16 | ## Using the image 17 | 18 | $ serverledge-cli create -f jsonFunc --memory 256 --runtime custom\ 19 | --custom_image 20 | $ serverledge-cli invoke -f jsonFunc --params_file input.json 21 | -------------------------------------------------------------------------------- /examples/jsonschema/executor.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler, HTTPServer 2 | import time 3 | import os 4 | import sys 5 | import importlib 6 | import json 7 | import jsonschema 8 | import function 9 | 10 | hostName = "0.0.0.0" 11 | serverPort = 8080 12 | 13 | 14 | class Executor(BaseHTTPRequestHandler): 15 | def do_POST(self): 16 | content_length = int(self.headers['Content-Length']) 17 | post_data = self.rfile.read(content_length) 18 | request = json.loads(post_data.decode('utf-8')) 19 | 20 | if not "invoke" in self.path: 21 | self.send_response(404) 22 | self.end_headers() 23 | return 24 | 25 | try: 26 | params = request["Params"] 27 | except: 28 | params = {} 29 | 30 | if "context" in os.environ: 31 | context = json.loads(os.environ["CONTEXT"]) 32 | else: 33 | context = {} 34 | 35 | 36 | response = {} 37 | 38 | try: 39 | result = function.handler(params, context) 40 | response["Result"] = json.dumps(result) 41 | response["Success"] = True 42 | except Exception as e: 43 | print(e, file=sys.stderr) 44 | response["Success"] = False 45 | 46 | self.send_response(200) 47 | self.send_header("Content-type", "application/json") 48 | self.end_headers() 49 | self.wfile.write(bytes(json.dumps(response), "utf-8")) 50 | 51 | 52 | 53 | if __name__ == "__main__": 54 | srv = HTTPServer((hostName, serverPort), Executor) 55 | try: 56 | srv.serve_forever() 57 | except KeyboardInterrupt: 58 | pass 59 | srv.server_close() 60 | 61 | -------------------------------------------------------------------------------- /examples/jsonschema/function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import jsonschema 3 | 4 | schema = { 5 | "type" : "object", 6 | "properties" : { 7 | "age" : {"type" : "number"}, 8 | "name" : {"type" : "string"}, 9 | "company" : {"type" : "string"}, 10 | }, 11 | } 12 | 13 | def handler (params, context): 14 | try: 15 | # validate json comprised in input 16 | jsonschema.validate(instance=params, schema=schema) 17 | result = {"Validation": True} 18 | except Exception as e: 19 | print(e) 20 | result = {"Validation": False, "Error": str(e)} 21 | return result 22 | -------------------------------------------------------------------------------- /examples/jsonschema/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Gabriele" 3 | } 4 | -------------------------------------------------------------------------------- /examples/local_offloading/README.md: -------------------------------------------------------------------------------- 1 | To test the offloading mechanism in a single (local) machine, 2 | we can run two Serverledge nodes on the same host, allowing only one of 3 | the nodes (i.e., the Cloud) to execute functions. 4 | 5 | Run the following commands from the root directory of the repository: 6 | 7 | 1. Start Etcd 8 | 9 | bash scripts/start-etcd.sh 10 | 11 | 2. Start the "Edge" node 12 | 13 | bin/serverledge examples/local_offloading/confEdge.yaml 14 | 15 | 3. Start the "Cloud" node 16 | 17 | bin/serverledge examples/local_offloading/confCloud.yaml 18 | 19 | 4. Create and invoke a function 20 | 21 | bin/serverledge-cli create -f func --memory 256 --src examples/isprime.py --runtime python310 --handler "isprime.handler" 22 | bin/serverledge-cli invoke -f func -p "n:17" 23 | 24 | -------------------------------------------------------------------------------- /examples/local_offloading/confCloud.yaml: -------------------------------------------------------------------------------- 1 | api.port: 1326 2 | cloud: true 3 | container.pool.memory: 2048 4 | scheduler.policy: localonly 5 | -------------------------------------------------------------------------------- /examples/local_offloading/confEdge.yaml: -------------------------------------------------------------------------------- 1 | scheduler.policy: cloudonly 2 | cloud.server.url: "http://127.0.0.1:1326" 3 | 4 | -------------------------------------------------------------------------------- /examples/noop.py: -------------------------------------------------------------------------------- 1 | def handler(params, context): 2 | return None 3 | -------------------------------------------------------------------------------- /examples/prometheus_config.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 3 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 4 | 5 | 6 | scrape_configs: 7 | - job_name: "serverledge" 8 | # metrics_path defaults to '/metrics' 9 | # scheme defaults to 'http'. 10 | static_configs: 11 | - targets: ["192.168.1.27:2112"] -------------------------------------------------------------------------------- /examples/sieve.js: -------------------------------------------------------------------------------- 1 | // Example ported from TinyFaaS (https://github.com/OpenFogStack/tinyFaaS/blob/master/examples/sieve-of-erasthostenes/index.js) 2 | // 3 | module.exports = (params, ctx) => { 4 | var max; 5 | 6 | if (params["n"] == undefined) { 7 | max = 1000 8 | } else { 9 | max = parseInt(params["n"],10); 10 | } 11 | 12 | let sieve = [], i, j, primes = []; 13 | for (i = 2; i <= max; ++i) { 14 | 15 | if (!sieve[i]) { 16 | primes.push(i); 17 | for (j = i << 1; j <= max; j += i) { 18 | sieve[j] = true; 19 | } 20 | } 21 | } 22 | 23 | result = {"N": max, "Primes": primes} 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /examples/sleeper.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def handler(params, context): 5 | n = params["n"] 6 | return ''.join(sleeper(int(n))) 7 | 8 | 9 | def sleeper(n): 10 | time.sleep(n) 11 | 12 | return "awake" -------------------------------------------------------------------------------- /examples/summarize.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import Counter 3 | 4 | 5 | def handler(params, context): 6 | return summarize_text(params["InputText"]) 7 | 8 | 9 | def summarize_text(text, num_sentences=2): 10 | # Split the text into sentences 11 | sentences = re.split(r'(? word.trim()) // Remove leading and trailing spaces from each word 10 | .filter(word => word !== '') // Filter out empty words 11 | .reduce((count, word) => count + 1, 0); // Reduce to count the words 12 | } 13 | 14 | module.exports = handler; -------------------------------------------------------------------------------- /examples/wordCountInput.json: -------------------------------------------------------------------------------- 1 | { 2 | "InputText": "Word counting is a useful technique for analyzing text data. It helps in various natural language processing tasks. In this example, we are testing the wordCount function in JavaScript. It should accurately count the number of words in this text. Counting words can be a fundamental step in text analysis.", 3 | "Task": true 4 | } -------------------------------------------------------------------------------- /examples/workflow-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "A simple state machine", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "FirstState": { 6 | "Comment": "The first task", 7 | "Type": "Task", 8 | "Resource": "inc", 9 | "Next": "SecondState" 10 | }, 11 | "SecondState": { 12 | "Comment": "The second task", 13 | "Type": "Task", 14 | "Resource": "inc", 15 | "Next": "Final" 16 | }, 17 | "Final": { 18 | "Comment": "The end task", 19 | "Type": "Task", 20 | "Resource": "inc", 21 | "End": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /images/base-alpine/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine AS build 2 | 3 | WORKDIR /sedge 4 | 5 | COPY . ./ 6 | RUN go mod download 7 | 8 | RUN CGO_ENABLED=0 go build -o /executor ./cmd/executor/executor.go 9 | 10 | 11 | 12 | 13 | FROM alpine:3 14 | 15 | WORKDIR / 16 | 17 | COPY --from=build /executor / 18 | RUN mkdir -p /app 19 | 20 | CMD /executor 21 | -------------------------------------------------------------------------------- /images/nodejs17ng/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17-alpine 2 | 3 | WORKDIR / 4 | 5 | COPY images/nodejs17ng/executor.js / 6 | RUN mkdir -p /app 7 | 8 | CMD node /executor.js 9 | -------------------------------------------------------------------------------- /images/nodejs17ng/executor.js: -------------------------------------------------------------------------------- 1 | let path = require('path'); 2 | var http = require('http'); 3 | 4 | http.createServer(async (request, response) => { 5 | 6 | if (request.method !== 'POST') { 7 | response.writeHead(404); 8 | response.end('Invalid request method'); 9 | } else { 10 | const buffers = []; 11 | 12 | for await (const chunk of request) { 13 | buffers.push(chunk); 14 | } 15 | 16 | const data = Buffer.concat(buffers).toString(); 17 | const contentType = 'application/json'; 18 | 19 | try { 20 | const reqbody = JSON.parse(data); 21 | 22 | var handler = reqbody["Handler"] 23 | var handler_dir = reqbody["HandlerDir"] 24 | var params = reqbody["Params"] 25 | var return_output = reqbody["ReturnOutput"] 26 | 27 | var context = {} 28 | if (process.env.CONTEXT !== "undefined") { 29 | context = process.env.CONTEXT 30 | } 31 | 32 | let h = require(path.join(handler_dir, handler)) 33 | 34 | result = h(params, context) 35 | 36 | resp = {} 37 | resp["Result"] = JSON.stringify(result); 38 | resp["Success"] = true 39 | if (return_output === true) { 40 | resp["Output"] = "Output capture not supported for this runtime yet." 41 | } else { 42 | resp["Output"] = "" 43 | } 44 | 45 | 46 | response.writeHead(200, { 'Content-Type': contentType }); 47 | response.end(JSON.stringify(resp), 'utf-8'); 48 | } catch (error) { 49 | resp = {} 50 | resp["Success"] = false 51 | resp["Output"] = "Output capture not supported for this runtime yet." 52 | response.writeHead(500, { 'Content-Type': contentType }); 53 | response.end(JSON.stringify(resp), 'utf-8'); 54 | } 55 | } 56 | 57 | }).listen(8080); 58 | console.log('Server running'); 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /images/python310/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine3.14 2 | 3 | WORKDIR / 4 | 5 | COPY images/python310/executor.py / 6 | RUN mkdir -p /app 7 | 8 | CMD python3 /executor.py 9 | -------------------------------------------------------------------------------- /images/python310/executor.py: -------------------------------------------------------------------------------- 1 | # Python 3 server example 2 | from socketserver import ThreadingMixIn 3 | from http.server import BaseHTTPRequestHandler, HTTPServer 4 | import time 5 | import os 6 | import sys 7 | import importlib 8 | import json 9 | 10 | hostName = "0.0.0.0" 11 | serverPort = 8080 12 | 13 | #executed_modules = {} 14 | added_dirs = {} 15 | 16 | from io import StringIO 17 | import sys 18 | 19 | class CaptureOutput: 20 | def __enter__(self): 21 | self._stdout_output = '' 22 | self._stderr_output = '' 23 | 24 | self._stdout = sys.stdout 25 | sys.stdout = StringIO() 26 | 27 | self._stderr = sys.stderr 28 | sys.stderr = StringIO() 29 | 30 | return self 31 | 32 | def __exit__(self, *args): 33 | self._stdout_output = sys.stdout.getvalue() 34 | sys.stdout = self._stdout 35 | 36 | self._stderr_output = sys.stderr.getvalue() 37 | sys.stderr = self._stderr 38 | 39 | def get_stdout(self): 40 | return self._stdout_output 41 | 42 | def get_stderr(self): 43 | return self._stderr_output 44 | 45 | 46 | class ThreadingSimpleServer(ThreadingMixIn, HTTPServer): 47 | pass 48 | 49 | class Executor(BaseHTTPRequestHandler): 50 | def do_POST(self): 51 | content_length = int(self.headers['Content-Length']) 52 | post_data = self.rfile.read(content_length) 53 | request = json.loads(post_data.decode('utf-8')) 54 | 55 | if not "invoke" in self.path: 56 | self.send_response(404) 57 | self.end_headers() 58 | return 59 | 60 | handler = request["Handler"] 61 | handler_dir = request["HandlerDir"] 62 | 63 | try: 64 | params = request["Params"] 65 | except: 66 | params = {} 67 | 68 | if "context" in os.environ: 69 | context = json.loads(os.environ["CONTEXT"]) 70 | else: 71 | context = {} 72 | 73 | if not handler_dir in added_dirs: 74 | sys.path.insert(1, handler_dir) 75 | added_dirs[handler_dir] = True 76 | 77 | # Get module name 78 | module,func_name = os.path.splitext(handler) 79 | func_name = func_name[1:] # strip initial dot 80 | loaded_mod = None 81 | 82 | return_output = bool(request["ReturnOutput"]) 83 | 84 | response = {} 85 | 86 | try: 87 | # Call function 88 | if loaded_mod is None: 89 | loaded_mod = importlib.import_module(module) 90 | 91 | if not return_output: 92 | result = getattr(loaded_mod, func_name)(params, context) 93 | response["Output"] = "" 94 | else: 95 | with CaptureOutput() as capturer: 96 | result = getattr(loaded_mod, func_name)(params, context) 97 | response["Output"] = str(capturer.get_stdout()) + "\n" + str(capturer.get_stderr()) 98 | 99 | response["Result"] = json.dumps(result) 100 | response["Success"] = True 101 | except Exception as e: 102 | print(e, file=sys.stderr) 103 | response["Success"] = False 104 | 105 | self.send_response(200) 106 | self.send_header("Content-type", "application/json") 107 | self.end_headers() 108 | self.wfile.write(bytes(json.dumps(response), "utf-8")) 109 | 110 | 111 | 112 | if __name__ == "__main__": 113 | webServer = ThreadingSimpleServer((hostName, serverPort), Executor) 114 | print("Server started http://%s:%s" % (hostName, serverPort)) 115 | 116 | try: 117 | webServer.serve_forever() 118 | except KeyboardInterrupt: 119 | pass 120 | 121 | webServer.server_close() 122 | print("Server stopped.") 123 | 124 | -------------------------------------------------------------------------------- /internal/api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "time" 12 | 13 | "github.com/labstack/echo/v4" 14 | "github.com/labstack/echo/v4/middleware" 15 | "github.com/serverledge-faas/serverledge/internal/cache" 16 | "github.com/serverledge-faas/serverledge/internal/config" 17 | "github.com/serverledge-faas/serverledge/internal/node" 18 | "github.com/serverledge-faas/serverledge/internal/registration" 19 | "github.com/serverledge-faas/serverledge/internal/scheduling" 20 | ) 21 | 22 | func StartAPIServer(e *echo.Echo) { 23 | e.Use(middleware.Recover()) 24 | 25 | // Routes 26 | e.POST("/invoke/:fun", InvokeFunction) 27 | e.POST("/create", CreateOrUpdateFunction) 28 | e.POST("/update", CreateOrUpdateFunction) 29 | e.POST("/delete", DeleteFunction) 30 | e.GET("/function", GetFunctions) 31 | e.GET("/poll/:reqId", PollAsyncResult) 32 | e.GET("/status", GetServerStatus) 33 | // Workflow routes 34 | e.POST("/workflow/invoke/:workflow", InvokeWorkflow) 35 | e.POST("/workflow/create", CreateWorkflowFromASL) 36 | e.POST("/workflow/import", CreateWorkflow) 37 | e.POST("/workflow/delete", DeleteWorkflow) 38 | e.GET("/workflow/list", GetWorkflows) 39 | 40 | // Start server 41 | portNumber := config.GetInt(config.API_PORT, 1323) 42 | e.HideBanner = true 43 | 44 | if err := e.Start(fmt.Sprintf(":%d", portNumber)); err != nil && !errors.Is(err, http.ErrServerClosed) { 45 | e.Logger.Fatal("shutting down the server") 46 | } 47 | } 48 | 49 | func CacheSetup() { 50 | //todo fix default values 51 | 52 | // setup cache space 53 | cache.Size = config.GetInt(config.CACHE_SIZE, 100) 54 | 55 | cache.Persist = config.GetBool(config.CACHE_PERSISTENCE, true) 56 | //setup cleanup interval 57 | d := config.GetInt(config.CACHE_CLEANUP, 60) 58 | interval := time.Duration(d) 59 | cache.CleanupInterval = interval * time.Second 60 | 61 | //setup default expiration time 62 | d = config.GetInt(config.CACHE_ITEM_EXPIRATION, 60) 63 | expirationInterval := time.Duration(d) 64 | cache.DefaultExp = expirationInterval * time.Second 65 | 66 | //cache first creation 67 | cache.GetCacheInstance() 68 | } 69 | 70 | func RegisterTerminationHandler(r *registration.Registry, e *echo.Echo) { 71 | c := make(chan os.Signal) 72 | signal.Notify(c, os.Interrupt) 73 | 74 | go func() { 75 | select { 76 | case sig := <-c: 77 | fmt.Printf("Got %s signal. Terminating...\n", sig) 78 | node.ShutdownAllContainers() 79 | 80 | // deregister from etcd; server should be unreachable 81 | err := r.Deregister() 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | 86 | //stop container janitor 87 | node.StopJanitor() 88 | 89 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 90 | defer cancel() 91 | if err := e.Shutdown(ctx); err != nil { 92 | e.Logger.Fatal(err) 93 | } 94 | 95 | os.Exit(0) 96 | } 97 | }() 98 | } 99 | 100 | func CreateSchedulingPolicy() scheduling.Policy { 101 | policyConf := config.GetString(config.SCHEDULING_POLICY, "default") 102 | log.Printf("Configured policy: %s\n", policyConf) 103 | if policyConf == "cloudonly" { 104 | return &scheduling.CloudOnlyPolicy{} 105 | } else if policyConf == "edgecloud" { 106 | return &scheduling.CloudEdgePolicy{} 107 | } else if policyConf == "edgeonly" { 108 | return &scheduling.EdgePolicy{} 109 | } else { // default, localonly 110 | return &scheduling.DefaultLocalPolicy{} 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /internal/asl/catch.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import ( 4 | "github.com/serverledge-faas/serverledge/internal/types" 5 | "golang.org/x/exp/slices" 6 | ) 7 | 8 | // Catch is a field in Task, Parallel and Map states. When a state reports an error and either there is no Retrier, or retries have failed to resolve the error, the interpreter scans through the Catchers in array order, and when the Error Name appears in the value of a Catcher’s "ErrorEquals" field, transitions the machine to the state named in the value of the "Next" field. The reserved name "States.ALL" appearing in a Retrier’s "ErrorEquals" field is a wildcard and matches any Error Name. 9 | type Catch struct { 10 | ErrorEquals []string 11 | ResultPath string 12 | Next string 13 | } 14 | 15 | func (c *Catch) Equals(cmp types.Comparable) bool { 16 | c2 := cmp.(*Catch) 17 | 18 | return slices.Equal(c.ErrorEquals, c2.ErrorEquals) && 19 | c.ResultPath == c2.ResultPath && 20 | c.Next == c2.Next 21 | } 22 | 23 | type CanCatch interface { 24 | GetCatchOpt() Catch 25 | } 26 | 27 | func NoCatch() *Catch { 28 | return &Catch{} 29 | } 30 | -------------------------------------------------------------------------------- /internal/asl/choice.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/buger/jsonparser" 7 | "github.com/labstack/gommon/log" 8 | "github.com/serverledge-faas/serverledge/internal/types" 9 | ) 10 | 11 | type ChoiceState struct { 12 | Type StateType // Necessary 13 | Choices []ChoiceRule // Necessary. All DataTestExpression must be State Machine with an end, like workflow.ChoiceNode(s). 14 | InputPath Path // Optional 15 | OutputPath Path // Optional 16 | // Default is the default state to execute when no other DataTestExpression matches 17 | Default string // Optional, but to avoid errors it is highly recommended. 18 | } 19 | 20 | func (c *ChoiceState) ParseFrom(jsonData []byte) (State, error) { 21 | c.Type = StateType(JsonExtractStringOrDefault(jsonData, "Type", "Choice")) 22 | c.InputPath = JsonExtractRefPathOrDefault(jsonData, "InputPath", "") 23 | c.OutputPath = JsonExtractRefPathOrDefault(jsonData, "OutputPath", "") 24 | c.Default = JsonExtractStringOrDefault(jsonData, "Default", "") 25 | 26 | choiceRules := make([]ChoiceRule, 0) 27 | 28 | choices, errChoice := JsonExtract(jsonData, "Choices") 29 | if errChoice != nil { 30 | return nil, fmt.Errorf("failed to parse Choices %v", errChoice) 31 | } 32 | 33 | _, _ = jsonparser.ArrayEach(choices, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { 34 | cr, errR := ParseRule(value) 35 | if errR != nil { 36 | log.Errorf("failed to parse choice rule %d: %v", offset, err) 37 | return 38 | } 39 | choiceRules = append(choiceRules, cr) 40 | }) 41 | //if errArr != nil { 42 | // return nil, fmt.Errorf("error %v when parsing choice rule %s", errArr, choices[:offset]) 43 | //} 44 | c.Choices = choiceRules 45 | 46 | return c, nil 47 | } 48 | 49 | func (c *ChoiceState) Validate(stateNames []string) error { 50 | if c.Default == "" { 51 | log.Warn("Default choice not specified") 52 | } 53 | return nil 54 | } 55 | 56 | func NewEmptyChoice() *ChoiceState { 57 | return &ChoiceState{ 58 | Type: Choice, 59 | Choices: []ChoiceRule{}, 60 | InputPath: "", 61 | OutputPath: "", 62 | Default: "", 63 | } 64 | } 65 | 66 | func (c *ChoiceState) GetType() StateType { 67 | return Choice 68 | } 69 | 70 | // GetNext for ChoiceState returns the Default branch instead of next 71 | func (c *ChoiceState) GetNext() (string, bool) { 72 | if c.Default != "" { 73 | return c.Default, true 74 | } 75 | return "", false 76 | } 77 | 78 | // IsEndState always returns true for a ChoiceState, because it is always a terminal state. 79 | func (c *ChoiceState) IsEndState() bool { 80 | return true 81 | } 82 | 83 | func (c *ChoiceState) Equals(cmp types.Comparable) bool { 84 | c2 := cmp.(*ChoiceState) 85 | 86 | if len(c.Choices) != len(c2.Choices) { 87 | return false 88 | } 89 | 90 | for i, c1 := range c.Choices { 91 | if !c1.Equals(c2.Choices[i]) { 92 | return false 93 | } 94 | } 95 | 96 | return c.Type == c2.Type && 97 | c.InputPath == c2.InputPath && 98 | c.OutputPath == c2.OutputPath && 99 | c.Default == c2.Default 100 | } 101 | 102 | func (c *ChoiceState) String() string { 103 | str := fmt.Sprint("{", 104 | "\n\t\t\tType: ", c.Type, 105 | "\n\t\t\tDefault: ", c.Default, 106 | "\n\t\t\tChoices: [") 107 | for i, c1 := range c.Choices { 108 | str += c1.String() 109 | if i < len(c.Choices)-1 { 110 | str += "," 111 | } 112 | } 113 | str += "\n\t\t\t]\n" 114 | 115 | if c.InputPath != "" { 116 | str += fmt.Sprintf("\t\t\tInputPath: %s\n", c.InputPath) 117 | } 118 | if c.OutputPath != "" { 119 | str += fmt.Sprintf("\t\t\tOutputPath: %s\n", c.OutputPath) 120 | } 121 | str += "\t\t}" 122 | return str 123 | } 124 | -------------------------------------------------------------------------------- /internal/asl/fail.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/serverledge-faas/serverledge/internal/types" 7 | ) 8 | 9 | type FailState struct { 10 | Type StateType 11 | // Error is a error identifier to be used in a retry State 12 | Error string 13 | ErrorPath Path 14 | // Cause is a human-readable message 15 | Cause string 16 | CausePath Path 17 | } 18 | 19 | func (f *FailState) Validate(stateNames []string) error { 20 | if f.Error != "" && f.ErrorPath != "" { 21 | return fmt.Errorf("the Error and ErrorPath fields cannot be both set at the same time") 22 | } 23 | 24 | if f.Cause != "" && f.CausePath != "" { 25 | return fmt.Errorf("the Cause and CausePath fields cannot be both set at the same time") 26 | } 27 | return nil 28 | } 29 | 30 | func (f *FailState) IsEndState() bool { 31 | return true 32 | } 33 | 34 | func (f *FailState) Equals(cmp types.Comparable) bool { 35 | f2 := cmp.(*FailState) 36 | return f.Type == f2.Type && 37 | f.Error == f2.Error && 38 | f.ErrorPath == f2.ErrorPath && 39 | f.Cause == f2.Cause && 40 | f.CausePath == f2.CausePath 41 | } 42 | 43 | func NewEmptyFail() *FailState { 44 | return &FailState{ 45 | Type: Fail, 46 | } 47 | } 48 | 49 | func (f *FailState) ParseFrom(jsonData []byte) (State, error) { 50 | f.Error = JsonExtractStringOrDefault(jsonData, "Error", "") 51 | f.ErrorPath = JsonExtractRefPathOrDefault(jsonData, "ErrorPath", "") 52 | f.Cause = JsonExtractStringOrDefault(jsonData, "Cause", "") 53 | f.CausePath = JsonExtractRefPathOrDefault(jsonData, "CausePath", "") 54 | return f, nil 55 | } 56 | 57 | func (f *FailState) GetType() StateType { 58 | return Fail 59 | } 60 | 61 | func (f *FailState) String() string { 62 | str := fmt.Sprint("{", 63 | "\n\t\t\tType: ", f.Type, 64 | "\n") 65 | if f.Error != "" { 66 | str += fmt.Sprintf("\t\t\tError: %s\n", f.Error) 67 | } 68 | if f.ErrorPath != "" { 69 | str += fmt.Sprintf("\t\t\tErrorPath: %s\n", f.ErrorPath) 70 | } 71 | if f.Cause != "" { 72 | str += fmt.Sprintf("\t\t\tCause: %s\n", f.Cause) 73 | } 74 | if f.CausePath != "" { 75 | str += fmt.Sprintf("\t\t\tCausePath: %s\n", f.CausePath) 76 | } 77 | str += "\t\t}" 78 | return str 79 | } 80 | 81 | func (f *FailState) GetError() string { 82 | if f.Error != "" { 83 | return f.Error 84 | } else if f.ErrorPath != "" { 85 | return string(f.ErrorPath) // will be evaluated at run time 86 | } else { 87 | return "GenericError" 88 | } 89 | } 90 | 91 | func (f *FailState) GetCause() string { 92 | if f.Cause != "" { 93 | return f.Cause 94 | } else if f.CausePath != "" { 95 | return string(f.CausePath) // will be evaluated at run time 96 | } else { 97 | return "Execution failed due to a generic error" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/asl/json.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/buger/jsonparser" 7 | ) 8 | 9 | func JsonHasKey(json []byte, key string) bool { 10 | _, dataType, _, err := jsonparser.Get(json, key) 11 | if err != nil || dataType == jsonparser.NotExist { 12 | return false 13 | } 14 | return true 15 | } 16 | 17 | func JsonHasAllKeys(json []byte, keys ...string) bool { 18 | for _, key := range keys { 19 | if !JsonHasKey(json, key) { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | 26 | func JsonHasOneKey(json []byte, keys ...string) bool { 27 | for _, key := range keys { 28 | if JsonHasKey(json, key) { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | func JsonExtract(json []byte, key string) ([]byte, error) { 36 | value, _, _, err := jsonparser.Get(json, key) 37 | if err != nil { 38 | return []byte(""), err 39 | } 40 | return value, nil 41 | } 42 | 43 | func JsonExtractString(json []byte, key string) (string, error) { 44 | value, _, _, err := jsonparser.Get(json, key) 45 | if err != nil { 46 | return "", err 47 | } 48 | return string(value), nil 49 | } 50 | 51 | func JsonExtractStringOrDefault(json []byte, key string, def string) string { 52 | value, _, _, err := jsonparser.Get(json, key) 53 | if err != nil { 54 | return def 55 | } 56 | return string(value) 57 | } 58 | 59 | func JsonExtractObjectOrDefault(json []byte, key string, def interface{}) interface{} { 60 | value, _, _, err := jsonparser.Get(json, key) 61 | if err != nil { 62 | return def 63 | } 64 | return value 65 | } 66 | 67 | func JsonTryExtractRefPath(json []byte, key string) (Path, error) { 68 | value, d, _, err := jsonparser.Get(json, key) 69 | if err != nil && d != jsonparser.NotExist { 70 | return "", err 71 | } 72 | path, errO := NewReferencePath(string(value)) 73 | if errO != nil { 74 | return "", err 75 | } 76 | return path, nil 77 | } 78 | 79 | func JsonExtractRefPathOrDefault(json []byte, key string, def Path) Path { 80 | value, _, _, err := jsonparser.Get(json, key) 81 | if err != nil { 82 | return def 83 | } 84 | path, errO := NewReferencePath(string(value)) 85 | if errO != nil { 86 | return def 87 | } 88 | return path 89 | } 90 | 91 | func JsonExtractIntOrDefault(json []byte, key string, def int) int { 92 | value, _, _, err := jsonparser.Get(json, key) 93 | if err != nil { 94 | return def 95 | } 96 | i, err := strconv.Atoi(string(value)) 97 | if err != nil { 98 | return def 99 | } 100 | return i 101 | } 102 | 103 | func JsonExtractOrNil(json []byte, key string) interface{} { 104 | value, _, _, err := jsonparser.Get(json, key) 105 | if err != nil { 106 | return nil 107 | } 108 | return string(value) 109 | } 110 | 111 | // JsonExtractBool extracts a boolean value with the specified key. If key does not exist, returns false 112 | func JsonExtractBool(json []byte, key string) bool { 113 | value, err := jsonparser.GetBoolean(json, key) 114 | if err != nil { 115 | return false 116 | } 117 | return value 118 | } 119 | 120 | func JsonNumberOfKeys(json []byte) int { 121 | num := 0 122 | 123 | _ = jsonparser.ObjectEach(json, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error { 124 | num++ 125 | return nil 126 | }) 127 | return num 128 | } 129 | -------------------------------------------------------------------------------- /internal/asl/map.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import "github.com/serverledge-faas/serverledge/internal/types" 4 | 5 | type MapState struct { 6 | Type StateType 7 | InputPath Path 8 | // ItemsPath is a Reference Path identifying where in the effective input the array field is found. 9 | ItemsPath Path 10 | // ItemProcessor is an object that defines a state machine which will process each item or batch of items of the array 11 | ItemProcessor *StateMachine 12 | // ItemReader is an object that specifies where to read the items instead of from the effective input 13 | ItemReader *ItemReaderConf // optional 14 | // Parameters is like ItemSelector (but is deprecated) 15 | Parameters string 16 | // ItemSelector is an object that overrides each single element of the item array 17 | ItemSelector string // optional 18 | // ItemBatcher is an object that specifies how to batch the items for the ItemProcessor 19 | ItemBatcher string // optional 20 | // ResultWriter is an object that specifies where to write the results instead of to the Map state's result 21 | ResultWriter string // optional 22 | // MaxConcurrency is an integer that provides an upper bound on how many invocations of the Iterator may run in parallel 23 | MaxConcurrency uint32 24 | // ToleratedFailurePercentage is an integer that provides an upper bound on the percentage of items that may fail 25 | ToleratedFailurePercentage uint8 26 | // ToleratedFailureCount is an integer that provides an upper bound on how many items may fail 27 | ToleratedFailureCount uint8 28 | // Next is the name of the next state to execute 29 | Next string 30 | // End if true, we do not need a Next 31 | End bool 32 | } 33 | 34 | func (m *MapState) Validate(stateNames []string) error { 35 | //TODO implement me 36 | panic("implement me") 37 | } 38 | 39 | func (m *MapState) IsEndState() bool { 40 | return m.End 41 | } 42 | 43 | func NewEmptyMap() *MapState { 44 | return &MapState{ 45 | Type: Map, 46 | InputPath: "", 47 | ItemsPath: "", 48 | ItemProcessor: nil, 49 | ItemReader: nil, 50 | Parameters: "", 51 | ItemSelector: "", 52 | ItemBatcher: "", 53 | ResultWriter: "", 54 | MaxConcurrency: 0, 55 | ToleratedFailurePercentage: 0, 56 | ToleratedFailureCount: 0, 57 | Next: "", 58 | End: false, 59 | } 60 | } 61 | 62 | type ItemReaderConf struct { 63 | Parameters PayloadTemplate 64 | Resource string 65 | MaxItems uint32 // not both 66 | MaxItemsPath Path // not both 67 | } 68 | 69 | func (m *MapState) GetResources() []string { 70 | res := make([]string, 0) 71 | processorResource := func() []string { 72 | if m.ItemProcessor == nil { 73 | return []string{} 74 | } 75 | return m.ItemProcessor.GetFunctionNames() 76 | }() 77 | res = append(res, processorResource...) 78 | 79 | if m.ItemReader != nil && m.ItemReader.Resource != "" { 80 | res = append(res, m.ItemReader.Resource) 81 | } 82 | 83 | return res 84 | } 85 | 86 | func (m *MapState) Equals(cmp types.Comparable) bool { 87 | m2 := cmp.(*MapState) 88 | return m.Type == m2.Type 89 | } 90 | 91 | func (m *MapState) ParseFrom(jsonData []byte) (State, error) { 92 | //TODO implement me 93 | panic("implement me") 94 | } 95 | 96 | func (m *MapState) GetNext() (string, bool) { 97 | if m.End == false { 98 | return m.Next, true 99 | } 100 | return "", false 101 | } 102 | 103 | func (m *MapState) GetType() StateType { 104 | return Map 105 | } 106 | 107 | func (m *MapState) String() string { 108 | return "Map" 109 | } 110 | -------------------------------------------------------------------------------- /internal/asl/parallel.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import "github.com/serverledge-faas/serverledge/internal/types" 4 | 5 | type ParallelState struct { 6 | Type StateType 7 | Branches []*StateMachine 8 | Next string 9 | End bool 10 | } 11 | 12 | func (p *ParallelState) Validate(stateNames []string) error { 13 | //TODO implement me 14 | panic("implement me") 15 | } 16 | 17 | func (p *ParallelState) IsEndState() bool { 18 | return p.End 19 | } 20 | 21 | func (p *ParallelState) GetResources() []string { 22 | funcs := make([]string, 0) 23 | for _, branchStateMachine := range p.Branches { 24 | funcs = append(funcs, branchStateMachine.GetFunctionNames()...) 25 | } 26 | return funcs 27 | } 28 | 29 | func (p *ParallelState) Equals(cmp types.Comparable) bool { 30 | p2 := cmp.(*ParallelState) 31 | return p.Type == p2.Type 32 | } 33 | 34 | func NewEmptyParallel() *ParallelState { 35 | return &ParallelState{ 36 | Type: Parallel, 37 | Branches: make([]*StateMachine, 0), 38 | Next: "", 39 | End: false, 40 | } 41 | } 42 | 43 | func (p *ParallelState) ParseFrom(jsonData []byte) (State, error) { 44 | //TODO implement me 45 | panic("implement me") 46 | } 47 | 48 | func (p *ParallelState) GetNext() (string, bool) { 49 | if p.End == false { 50 | return p.Next, true 51 | } 52 | return "", false 53 | } 54 | 55 | func (p *ParallelState) GetType() StateType { 56 | return Parallel 57 | } 58 | 59 | // FIXME: improve 60 | func (p *ParallelState) String() string { 61 | return "Parallel" 62 | } 63 | -------------------------------------------------------------------------------- /internal/asl/pass.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import "github.com/serverledge-faas/serverledge/internal/types" 4 | 5 | type PassState struct { 6 | Type StateType 7 | Next string 8 | End bool 9 | } 10 | 11 | func (p *PassState) Validate(stateNames []string) error { 12 | //TODO implement me 13 | panic("implement me") 14 | } 15 | 16 | func (p *PassState) IsEndState() bool { 17 | return p.End 18 | } 19 | 20 | func (p *PassState) Equals(cmp types.Comparable) bool { 21 | p2 := cmp.(*PassState) 22 | return p.Type == p2.Type && p.Next == p2.Next && p.End == p2.End 23 | } 24 | 25 | func NewEmptyPass() *PassState { 26 | return &PassState{ 27 | Type: Pass, 28 | Next: "", 29 | End: false, 30 | } 31 | } 32 | 33 | func (p *PassState) ParseFrom(jsonData []byte) (State, error) { 34 | //TODO implement me 35 | panic("implement me") 36 | } 37 | 38 | func (p *PassState) GetNext() (string, bool) { 39 | if p.End == false { 40 | return p.Next, true 41 | } 42 | return "", false 43 | } 44 | 45 | func (p *PassState) GetType() StateType { 46 | return Pass 47 | } 48 | 49 | func (p *PassState) String() string { 50 | //TODO implement me 51 | panic("implement me") 52 | } 53 | -------------------------------------------------------------------------------- /internal/asl/path.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // Path is a string beginning with "$", used to identify components in a JSON text, in JSONPath format. 10 | // Used to navigate an input parameter for a state 11 | type Path string 12 | 13 | // NewReferencePath creates a new Reference Path, that starts with a $ character, separated by "." characters and 14 | // that does not contain the following characters '@' ',' ':' '?'. Used to define input or output parameters. 15 | func NewReferencePath(s string) (Path, error) { 16 | if s == "" || s == "$" { 17 | return Path(s), nil 18 | } 19 | if !strings.HasPrefix(s, "$.") { 20 | s = "$." + s 21 | } 22 | if strings.Contains(s, "@") || strings.Contains(s, ",") || strings.Contains(s, ":") || strings.Contains(s, "?") { 23 | return "", fmt.Errorf("A reference path should not contain any of the following characters: '@' ',' ':' '?' ") 24 | } 25 | return Path(s), nil 26 | } 27 | 28 | // IsReferencePath checks whether the input is a valid reference path string or not (e.g. starts with '$') 29 | func IsReferencePath(valpar interface{}) bool { 30 | if reflect.TypeOf(valpar).Kind() == reflect.String { 31 | s, ok := valpar.(string) 32 | if !ok { 33 | fmt.Printf("this should never happen: parameter has kind string, but is not a string") 34 | return false 35 | } 36 | return s == "$" || (strings.HasPrefix(s, "$.") && len(s) > 2) 37 | } 38 | return false 39 | } 40 | 41 | // RemoveDollar removes the leading '$.' from the reference path. It leaves subsequent '.' in it. 42 | func RemoveDollar(s string) string { 43 | if s == "" || s == "$" { 44 | return "" 45 | } else if strings.HasPrefix(s, "$.") { 46 | return s[2:] 47 | } 48 | return s 49 | } 50 | -------------------------------------------------------------------------------- /internal/asl/payload_template.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | type PayloadTemplate struct { 4 | json string 5 | } 6 | -------------------------------------------------------------------------------- /internal/asl/retry.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import ( 4 | "github.com/serverledge-faas/serverledge/internal/types" 5 | "golang.org/x/exp/slices" 6 | ) 7 | 8 | // Retry is a field in Task, Parallel and Map states which retries the state for a period or for a specified number of times 9 | type Retry struct { 10 | ErrorEquals []string 11 | IntervalSeconds int 12 | BackoffRate int 13 | MaxAttempts int 14 | } 15 | 16 | func (r *Retry) Equals(cmp types.Comparable) bool { 17 | r2 := cmp.(*Retry) 18 | return slices.Equal(r.ErrorEquals, r2.ErrorEquals) && 19 | r.IntervalSeconds == r2.IntervalSeconds && 20 | r.BackoffRate == r2.BackoffRate && 21 | r.MaxAttempts == r2.MaxAttempts 22 | } 23 | 24 | type CanRetry interface { 25 | GetRetryOpt() Retry 26 | } 27 | 28 | func NoRetry() *Retry { 29 | return &Retry{} 30 | } 31 | -------------------------------------------------------------------------------- /internal/asl/state.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/serverledge-faas/serverledge/internal/types" 7 | ) 8 | 9 | // State is the common interface for ASL states. StateTypes are Task, Parallel, Map, Pass, Wait, Choice, Succeed, Fail 10 | type State interface { 11 | fmt.Stringer 12 | types.Comparable 13 | Validatable 14 | GetType() StateType 15 | } 16 | 17 | // StateType for ASL states 18 | type StateType string 19 | 20 | const ( 21 | Task StateType = "Task" 22 | Parallel StateType = "Parallel" 23 | Map StateType = "Map" 24 | Pass StateType = "Pass" 25 | Wait StateType = "Wait" 26 | Choice StateType = "Choice" 27 | Succeed StateType = "Succeed" 28 | Fail StateType = "Fail" 29 | ) 30 | 31 | // InputPath is a field that indicates from where to read input 32 | type InputPath = string 33 | 34 | // OutputPath is a field that indicates where to write output 35 | type OutputPath = string 36 | 37 | // HasNext is an interface for non-terminal states. 38 | // Fail, Succeed and any other state with End=true are terminal states. 39 | // Fail and Succeed should not implement this interface 40 | // Should be implemented by Task, Parallel, Map, Pass and Wait 41 | type HasNext interface { 42 | // GetNext returns the next state, if exists. Otherwise, returns an empty string and false 43 | GetNext() (string, bool) 44 | } 45 | 46 | // CanEnd is an interface with a boolean method. If true, marks the state machine end. Valid for Task, Parallel, Map, Pass and Wait states 47 | type CanEnd interface { 48 | IsEndState() bool 49 | } 50 | 51 | // ResultPath is a string field that indicates the result path 52 | type ResultPath = string 53 | 54 | // Parameters is a JSON string payload template with names and values of input parameters for Task, Map and Parallel states 55 | type Parameters = string 56 | type HasParameters interface { 57 | GetParameters() Parameters 58 | } 59 | 60 | // ResultSelector is a reference path that indicates where to place the output relative to the raw input. 61 | type ResultSelector = string // TODO: what is that 62 | 63 | type HasResources interface { 64 | // GetResources returns all function names present in the State. The implementation could return duplicate functions 65 | GetResources() []string 66 | } 67 | 68 | type Parsable interface { 69 | fmt.Stringer 70 | ParseFrom(jsonData []byte) (State, error) 71 | } 72 | 73 | // Validatable checks every state of the state machine. Use it when the state machine is complete 74 | type Validatable interface { 75 | Validate(stateNames []string) error 76 | } 77 | -------------------------------------------------------------------------------- /internal/asl/succeed.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/serverledge-faas/serverledge/internal/types" 7 | ) 8 | 9 | type SucceedState struct { 10 | Type StateType // Necessary 11 | InputPath Path // Optional, default $ 12 | OutputPath Path // Optional, default $ 13 | } 14 | 15 | func (s *SucceedState) Validate(stateNames []string) error { 16 | return nil 17 | } 18 | 19 | func (s *SucceedState) IsEndState() bool { 20 | return true 21 | } 22 | 23 | func (s *SucceedState) Equals(cmp types.Comparable) bool { 24 | s2 := cmp.(*SucceedState) 25 | return s.Type == s2.Type && 26 | s.InputPath == s2.InputPath && 27 | s.OutputPath == s2.OutputPath 28 | } 29 | 30 | func NewEmptySucceed() *SucceedState { 31 | return &SucceedState{ 32 | Type: Succeed, 33 | } 34 | } 35 | 36 | func (s *SucceedState) ParseFrom(jsonData []byte) (State, error) { 37 | s.InputPath = JsonExtractRefPathOrDefault(jsonData, "InputPath", "") 38 | s.OutputPath = JsonExtractRefPathOrDefault(jsonData, "OutputPath", "") 39 | return s, nil 40 | } 41 | 42 | func (s *SucceedState) GetType() StateType { 43 | return Succeed 44 | } 45 | 46 | func (s *SucceedState) String() string { 47 | str := fmt.Sprint("{", 48 | "\n\t\t\tType: ", s.Type, 49 | "\n") 50 | if s.InputPath != "" { 51 | str += fmt.Sprintf("\t\t\tError: %s\n", s.InputPath) 52 | } 53 | if s.OutputPath != "" { 54 | str += fmt.Sprintf("\t\t\tErrorPath: %s\n", s.OutputPath) 55 | } 56 | str += "\t\t}" 57 | return str 58 | } 59 | -------------------------------------------------------------------------------- /internal/asl/wait.go: -------------------------------------------------------------------------------- 1 | package asl 2 | 3 | import "github.com/serverledge-faas/serverledge/internal/types" 4 | 5 | type WaitState struct { 6 | Type StateType 7 | Next string 8 | End bool 9 | } 10 | 11 | func (w *WaitState) ParseFrom(jsonData []byte) (State, error) { 12 | //TODO implement me 13 | panic("implement me") 14 | } 15 | 16 | func (w *WaitState) Validate(stateNames []string) error { 17 | //TODO implement me 18 | panic("implement me") 19 | } 20 | 21 | func NewEmptyWait() *WaitState { 22 | return &WaitState{ 23 | Type: Wait, 24 | } 25 | } 26 | 27 | func (w *WaitState) GetType() StateType { 28 | return Wait 29 | } 30 | 31 | func (w *WaitState) GetNext() (string, bool) { 32 | if w.End == false { 33 | return w.Next, true 34 | } 35 | return "", false 36 | } 37 | 38 | func (w *WaitState) IsEndState() bool { 39 | return w.End 40 | } 41 | 42 | func (w *WaitState) Equals(cmp types.Comparable) bool { 43 | w2 := cmp.(*WaitState) 44 | return w.Type == w2.Type 45 | } 46 | 47 | func (w *WaitState) String() string { 48 | //TODO implement me 49 | panic("implement me") 50 | } 51 | -------------------------------------------------------------------------------- /internal/cache/cache_handler.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | var ( 9 | Instance *Cache 10 | ) 11 | 12 | var lock = &sync.Mutex{} 13 | var ( 14 | DefaultExp time.Duration = 0 // default expiration 15 | CleanupInterval time.Duration = 0 //cleanup interval 16 | Size = 0 17 | Persist = false // default value used to test progress/partial datas. If false, they are only saved in local cache 18 | ) 19 | 20 | // GetCacheInstance : singleton implementation to retrieve THE cache 21 | func GetCacheInstance() *Cache { 22 | lock.Lock() 23 | defer lock.Unlock() 24 | 25 | if Instance == nil { 26 | 27 | Instance = New(DefaultExp, CleanupInterval, Size) // <-- thread safe 28 | } 29 | 30 | return Instance 31 | } 32 | -------------------------------------------------------------------------------- /internal/client/types.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/serverledge-faas/serverledge/internal/function" 5 | ) 6 | 7 | // InvocationRequest is an external invocation of a function (from API or CLI) 8 | type InvocationRequest struct { 9 | Params map[string]interface{} 10 | QoSClass function.ServiceClass 11 | QoSMaxRespT float64 12 | CanDoOffloading bool 13 | Async bool 14 | ReturnOutput bool 15 | } 16 | 17 | type PrewarmingRequest struct { 18 | Function string 19 | Instances int64 20 | ForceImagePull bool 21 | } 22 | 23 | // WorkflowInvocationRequest is an external invocation of a workflow (from API or CLI) 24 | type WorkflowInvocationRequest struct { 25 | Params map[string]interface{} 26 | QoS function.RequestQoS 27 | CanDoOffloading bool 28 | Async bool 29 | // NextNodes []string // DagNodeId 30 | // we do not add Progress here, only the next group of node that should execute 31 | // in case of choice node, we retrieve the progress for each taskId and execute only the one that is not in Skipped State 32 | // in case of fan out node, we retrieve all the progress and execute concurrently all the tasks in the group. 33 | // in case of fan in node, we retrieve periodically all the progress of the previous nodes and start the merging only when all previous node are completed. 34 | // or simply, we can get the N partialData for the Fan Out, coming from the previous nodes. 35 | // furthermore, we should be careful not to run multiple fanIn at the same time! 36 | } 37 | 38 | type WorkflowCreationRequest struct { 39 | Name string // Name of the new workflow 40 | ASLSrc string // Specification source in Amazon State Language (encoded in Base64) 41 | } 42 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "path/filepath" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | var DefaultConfigFileName = "serverledge-conf" 11 | 12 | // Get returns the configured value for a given key or the specified default. 13 | func Get(key string, defaultValue interface{}) interface{} { 14 | if viper.IsSet(key) { 15 | return viper.Get(key) 16 | } else { 17 | return defaultValue 18 | } 19 | } 20 | 21 | func GetInt(key string, defaultValue int) int { 22 | if viper.IsSet(key) { 23 | return viper.GetInt(key) 24 | } else { 25 | return defaultValue 26 | } 27 | } 28 | 29 | func GetFloat(key string, defaultValue float64) float64 { 30 | if viper.IsSet(key) { 31 | return viper.GetFloat64(key) 32 | } else { 33 | return defaultValue 34 | } 35 | } 36 | 37 | func GetString(key string, defaultValue string) string { 38 | if viper.IsSet(key) { 39 | return viper.GetString(key) 40 | } else { 41 | return defaultValue 42 | } 43 | } 44 | 45 | func GetBool(key string, defaultValue bool) bool { 46 | if viper.IsSet(key) { 47 | return viper.GetBool(key) 48 | } else { 49 | return defaultValue 50 | } 51 | } 52 | 53 | // ReadConfiguration reads a configuration file stored in one of the predefined paths. 54 | func ReadConfiguration(fileName string) { 55 | // paths where the config file can be placed 56 | viper.AddConfigPath("/etc/serverledge/") 57 | viper.AddConfigPath("$HOME/") 58 | viper.AddConfigPath(".") 59 | 60 | if fileName != "" { 61 | parentDir := filepath.Dir(fileName) 62 | baseName := filepath.Base(fileName) 63 | extension := filepath.Ext(baseName) 64 | baseNameNoExt := baseName[0 : len(baseName)-len(extension)] 65 | 66 | viper.SetConfigName(baseNameNoExt) //custom name of config file (without extension) 67 | viper.AddConfigPath(parentDir) 68 | } else { 69 | viper.SetConfigName(DefaultConfigFileName) // default name of config file (without extension) 70 | } 71 | 72 | if err := viper.ReadInConfig(); err != nil { 73 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 74 | // No configuration file parsed 75 | } else { 76 | log.Printf("Config file parsing failed!\n") 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/config/keys.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Etcd server hostname 4 | const ETCD_ADDRESS = "etcd.address" 5 | 6 | // exposed port for serverledge APIs 7 | const API_PORT = "api.port" 8 | const API_IP = "api.ip" 9 | 10 | // REMOTE SERVER URL 11 | const CLOUD_URL = "cloud.server.url" 12 | 13 | // Forces runtime container images to be pulled the first time they are used, 14 | // even if they are locally available (true/false). 15 | const FACTORY_REFRESH_IMAGES = "factory.images.refresh" 16 | 17 | // Amount of memory available for the container pool (in MB) 18 | const POOL_MEMORY_MB = "container.pool.memory" 19 | 20 | // CPUs available for the container pool (1.0 = 1 core) 21 | const POOL_CPUS = "container.pool.cpus" 22 | 23 | // periodically janitor wakes up and deletes expired containers 24 | const POOL_CLEANUP_PERIOD = "janitor.interval" 25 | 26 | // container expiration time 27 | const CONTAINER_EXPIRATION_TIME = "container.expiration" 28 | 29 | // cache capacity 30 | const CACHE_SIZE = "cache.size" 31 | 32 | // cache janitor interval (Seconds) : deletes expired items 33 | const CACHE_CLEANUP = "cache.cleanup" 34 | 35 | // default expiration time assigned to a cache item (Seconds) 36 | const CACHE_ITEM_EXPIRATION = "cache.expiration" 37 | 38 | // default policy is to persist cache (boolean). Use false in localonly deployments 39 | const CACHE_PERSISTENCE = "cache.persistence" 40 | 41 | // true if the current server is a remote cloud server 42 | const IS_IN_CLOUD = "cloud" 43 | 44 | // the area wich the server belongs to 45 | const REGISTRY_AREA = "registry.area" 46 | 47 | // short period: retrieve information about nearby edge-servers 48 | const REG_NEARBY_INTERVAL = "registry.nearby.interval" 49 | 50 | // long period for general monitoring inside the area 51 | const REG_MONITORING_INTERVAL = "registry.monitoring.interval" 52 | 53 | // registration TTL in seconds 54 | const REGISTRATION_TTL = "registry.ttl" 55 | 56 | // port for udp status listener 57 | const LISTEN_UDP_PORT = "registry.udp.port" 58 | 59 | // enable metrics system 60 | const METRICS_ENABLED = "metrics.enabled" 61 | 62 | const METRICS_PROMETHEUS_HOST = "metrics.prometheus.host" 63 | const METRICS_PROMETHEUS_PORT = "metrics.prometheus.port" 64 | 65 | // Scheduling policy to use 66 | // Possible values: "qosaware", "default", "cloudonly" 67 | const SCHEDULING_POLICY = "scheduler.policy" 68 | 69 | // Capacity of the queue (possibly) used by the scheduler 70 | const SCHEDULER_QUEUE_CAPACITY = "scheduler.queue.capacity" 71 | 72 | // Enables tracing 73 | const TRACING_ENABLED = "tracing.enabled" 74 | 75 | // Custom output file for traces 76 | const TRACING_OUTFILE = "tracing.outfile" 77 | -------------------------------------------------------------------------------- /internal/config/types.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type RemoteServerConf struct { 4 | Host string 5 | Port int 6 | } 7 | -------------------------------------------------------------------------------- /internal/container/factory.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // A Factory to create and manage container. 8 | type Factory interface { 9 | Create(string, *ContainerOptions) (ContainerID, error) 10 | CopyToContainer(ContainerID, io.Reader, string) error 11 | Start(ContainerID) error 12 | Destroy(ContainerID) error 13 | HasImage(string) bool 14 | PullImage(string) error 15 | GetIPAddress(ContainerID) (string, error) 16 | GetMemoryMB(id ContainerID) (int64, error) 17 | GetLog(id ContainerID) (string, error) 18 | } 19 | 20 | // ContainerOptions contains options for container creation. 21 | type ContainerOptions struct { 22 | Cmd []string 23 | Env []string 24 | MemoryMB int64 25 | CPUQuota float64 26 | } 27 | 28 | type ContainerID = string 29 | 30 | type Container struct { 31 | ID ContainerID 32 | RequestsCount int16 33 | ExpirationTime int64 34 | } 35 | 36 | // cf is the container factory for the node 37 | var cf Factory 38 | -------------------------------------------------------------------------------- /internal/container/runtimes.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | // RuntimeInfo contains information about a supported function runtime env. 4 | type RuntimeInfo struct { 5 | Image string 6 | InvocationCmd []string 7 | ConcurrencySupported bool 8 | } 9 | 10 | const CUSTOM_RUNTIME = "custom" 11 | 12 | var refreshedImages = map[string]bool{} 13 | 14 | var RuntimeToInfo = map[string]RuntimeInfo{ 15 | "python310": {"grussorusso/serverledge-python310", []string{"python", "/entrypoint.py"}, true}, 16 | "nodejs17ng": {"grussorusso/serverledge-nodejs17ng", []string{}, false}, 17 | } 18 | -------------------------------------------------------------------------------- /internal/executor/constants.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | const DEFAULT_EXECUTOR_PORT = 8080 4 | -------------------------------------------------------------------------------- /internal/executor/server.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | const resultFile = "/tmp/_executor_result.json" 14 | const paramsFile = "/tmp/_executor.params" 15 | 16 | func readExecutionResult(resultFile string) string { 17 | content, err := os.ReadFile(resultFile) 18 | if err != nil { 19 | log.Printf("%v\n", err) 20 | return "" 21 | } 22 | 23 | return string(content) 24 | } 25 | 26 | func InvokeHandler(w http.ResponseWriter, r *http.Request) { 27 | // Parse request 28 | reqDecoder := json.NewDecoder(r.Body) 29 | req := &InvocationRequest{} 30 | err := reqDecoder.Decode(req) 31 | if err != nil { 32 | http.Error(w, err.Error(), http.StatusInternalServerError) 33 | return 34 | } 35 | 36 | // Set environment variables 37 | err = os.Setenv("RESULT_FILE", resultFile) 38 | err = errors.Join(err, os.Setenv("HANDLER", req.Handler)) 39 | err = errors.Join(err, os.Setenv("HANDLER_DIR", req.HandlerDir)) 40 | params := req.Params 41 | if params == nil { 42 | err = errors.Join(err, os.Setenv("PARAMS_FILE", "")) 43 | } else { 44 | paramsB, _ := json.Marshal(req.Params) 45 | fileError := os.WriteFile(paramsFile, paramsB, 0644) 46 | if fileError != nil { 47 | log.Printf("Could not write parameters to %s\n", paramsFile) 48 | http.Error(w, fileError.Error(), http.StatusInternalServerError) 49 | return 50 | } 51 | err = errors.Join(err, os.Setenv("PARAMS_FILE", paramsFile)) 52 | } 53 | if err != nil { 54 | log.Printf("Error while setting environment variables: %s\n", err) 55 | } 56 | 57 | // Exec handler process 58 | cmd := req.Command 59 | if cmd == nil || len(cmd) < 1 { 60 | // this request is either invalid or uses a custom runtime 61 | // in the latter case, we find the command in the env 62 | customCmd, ok := os.LookupEnv("CUSTOM_CMD") 63 | if !ok { 64 | log.Printf("Invalid request!\n") 65 | http.Error(w, err.Error(), http.StatusBadRequest) 66 | return 67 | } 68 | 69 | cmd = strings.Split(customCmd, " ") 70 | } 71 | 72 | var resp *InvocationResult 73 | execCmd := exec.Command(cmd[0], cmd[1:]...) 74 | out, err := execCmd.CombinedOutput() 75 | if err != nil { 76 | log.Printf("cmd.Run() failed with %s\n", err) 77 | if req.ReturnOutput { 78 | resp = &InvocationResult{Success: false, Output: string(out)} 79 | } else { 80 | resp = &InvocationResult{Success: false, Output: ""} 81 | } 82 | } else { 83 | result := readExecutionResult(resultFile) 84 | 85 | if req.ReturnOutput { 86 | resp = &InvocationResult{true, result, string(out)} 87 | } else { 88 | resp = &InvocationResult{true, result, ""} 89 | } 90 | } 91 | 92 | w.Header().Set("Content-Type", "application/json") 93 | respBody, _ := json.Marshal(resp) 94 | _, err = w.Write(respBody) 95 | if err != nil { 96 | log.Printf("Error while writing response to HTTP %s\n", err) 97 | return 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/executor/types.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | // InvocationRequest is a struct used by the executor to effectively run the function 4 | type InvocationRequest struct { 5 | Command []string 6 | Params map[string]interface{} 7 | Handler string 8 | HandlerDir string 9 | ReturnOutput bool 10 | } 11 | 12 | type InvocationResult struct { 13 | Success bool 14 | Result string 15 | Output string 16 | } 17 | -------------------------------------------------------------------------------- /internal/function/request.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // Request represents a single function invocation, with a ReqId, reference to the Function, parameters and metrics data 10 | type Request struct { 11 | Ctx context.Context 12 | Fun *Function 13 | Params map[string]interface{} 14 | Arrival time.Time 15 | RequestQoS 16 | CanDoOffloading bool 17 | Async bool 18 | ReturnOutput bool 19 | } 20 | 21 | type RequestQoS struct { 22 | Class ServiceClass 23 | MaxRespT float64 24 | } 25 | 26 | type ExecutionReport struct { 27 | Result string 28 | ResponseTime float64 // time waited by the user to get the output: completion time - arrival time (offload + cold start + execution time) 29 | IsWarmStart bool 30 | InitTime float64 // time spent sleeping before initializing container 31 | OffloadLatency float64 // time spent offloading the request 32 | Duration float64 // execution (service) time 33 | SchedAction string 34 | Output string 35 | } 36 | 37 | type Response struct { 38 | Success bool 39 | ExecutionReport 40 | } 41 | 42 | type AsyncResponse struct { 43 | ReqId string 44 | } 45 | 46 | func (r *Request) Id() string { 47 | return r.Ctx.Value("ReqId").(string) 48 | } 49 | 50 | func (r *Request) String() string { 51 | return fmt.Sprintf("[%s] Rq-%s", r.Fun.Name, r.Id()) 52 | } 53 | 54 | type ServiceClass int64 55 | 56 | const ( 57 | LOW ServiceClass = 0 58 | HIGH_PERFORMANCE = 1 59 | HIGH_AVAILABILITY = 2 60 | ) 61 | -------------------------------------------------------------------------------- /internal/lb/lb.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "time" 11 | 12 | "github.com/serverledge-faas/serverledge/internal/config" 13 | "github.com/serverledge-faas/serverledge/internal/registration" 14 | "github.com/labstack/echo/v4" 15 | "github.com/labstack/echo/v4/middleware" 16 | ) 17 | 18 | var currentTargets []*middleware.ProxyTarget 19 | 20 | func newBalancer(targets []*middleware.ProxyTarget) middleware.ProxyBalancer { 21 | return middleware.NewRoundRobinBalancer(targets) 22 | } 23 | 24 | func StartReverseProxy(e *echo.Echo, region string) { 25 | targets, err := getTargets(region) 26 | if err != nil { 27 | log.Printf("Cannot connect to registry to retrieve targets: %v\n", err) 28 | os.Exit(2) 29 | } 30 | 31 | log.Printf("Initializing with %d targets.\n", len(targets)) 32 | balancer := newBalancer(targets) 33 | currentTargets = targets 34 | e.Use(middleware.Proxy(balancer)) 35 | 36 | go updateTargets(balancer, region) 37 | 38 | portNumber := config.GetInt(config.API_PORT, 1323) 39 | if err := e.Start(fmt.Sprintf(":%d", portNumber)); err != nil && !errors.Is(err, http.ErrServerClosed) { 40 | e.Logger.Fatal("shutting down the server") 41 | } 42 | } 43 | 44 | func getTargets(region string) ([]*middleware.ProxyTarget, error) { 45 | cloudNodes, err := registration.GetCloudNodes(region) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | targets := make([]*middleware.ProxyTarget, 0, len(cloudNodes)) 51 | for _, addr := range cloudNodes { 52 | log.Printf("Found target: %v\n", addr) 53 | // TODO: etcd should NOT contain URLs, but only host and port... 54 | parsedUrl, err := url.Parse(addr) 55 | if err != nil { 56 | return nil, err 57 | } 58 | targets = append(targets, &middleware.ProxyTarget{Name: addr, URL: parsedUrl}) 59 | } 60 | 61 | log.Printf("Found %d targets\n", len(targets)) 62 | 63 | return targets, nil 64 | } 65 | 66 | func updateTargets(balancer middleware.ProxyBalancer, region string) { 67 | for { 68 | time.Sleep(30 * time.Second) // TODO: configure 69 | 70 | targets, err := getTargets(region) 71 | if err != nil { 72 | log.Printf("Cannot update targets: %v\n", err) 73 | } 74 | 75 | toKeep := make([]bool, len(currentTargets)) 76 | for i := range currentTargets { 77 | toKeep[i] = false 78 | } 79 | for _, t := range targets { 80 | toAdd := true 81 | for i, curr := range currentTargets { 82 | if curr.Name == t.Name { 83 | toKeep[i] = true 84 | toAdd = false 85 | } 86 | } 87 | if toAdd { 88 | log.Printf("Adding %s\n", t.Name) 89 | balancer.AddTarget(t) 90 | } 91 | } 92 | 93 | toRemove := make([]string, 0) 94 | for i, curr := range currentTargets { 95 | if !toKeep[i] { 96 | log.Printf("Removing %s\n", curr.Name) 97 | toRemove = append(toRemove, curr.Name) 98 | } 99 | } 100 | for _, curr := range toRemove { 101 | balancer.RemoveTarget(curr) 102 | } 103 | 104 | currentTargets = targets 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "log" 5 | 6 | "net/http" 7 | 8 | "github.com/serverledge-faas/serverledge/internal/config" 9 | "github.com/serverledge-faas/serverledge/internal/node" 10 | 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/prometheus/client_golang/prometheus/promauto" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | ) 15 | 16 | var Enabled bool 17 | var registry = prometheus.NewRegistry() 18 | var nodeIdentifier string 19 | 20 | func Init() { 21 | if config.GetBool(config.METRICS_ENABLED, false) { 22 | log.Println("Metrics enabled.") 23 | Enabled = true 24 | } else { 25 | Enabled = false 26 | return 27 | } 28 | 29 | nodeIdentifier = node.NodeIdentifier 30 | registerGlobalMetrics() 31 | 32 | handler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{ 33 | EnableOpenMetrics: true}) 34 | http.Handle("/metrics", handler) 35 | err := http.ListenAndServe(":2112", nil) 36 | if err != nil { 37 | log.Printf("Listen and serve terminated with error: %s\n", err) 38 | return 39 | } 40 | } 41 | 42 | // Global metrics 43 | var ( 44 | CompletedInvocations = promauto.NewCounterVec(prometheus.CounterOpts{ 45 | Name: "sedge_completed_total", 46 | Help: "The total number of completed function invocations", 47 | }, []string{"node", "function"}) 48 | ExecutionTimes = promauto.NewHistogramVec(prometheus.HistogramOpts{ 49 | Name: "sedge_exectime", 50 | Help: "Function duration", 51 | Buckets: durationBuckets, 52 | }, 53 | []string{"node", "function"}) 54 | ) 55 | 56 | var durationBuckets = []float64{0.002, 0.005, 0.010, 0.02, 0.03, 0.05, 0.1, 0.15, 0.3, 0.6, 1.0} 57 | 58 | func AddCompletedInvocation(funcName string) { 59 | CompletedInvocations.With(prometheus.Labels{"function": funcName, "node": nodeIdentifier}).Inc() 60 | } 61 | func AddFunctionDurationValue(funcName string, duration float64) { 62 | ExecutionTimes.With(prometheus.Labels{"function": funcName, "node": nodeIdentifier}).Observe(duration) 63 | } 64 | 65 | func registerGlobalMetrics() { 66 | registry.MustRegister(CompletedInvocations) 67 | registry.MustRegister(ExecutionTimes) 68 | } 69 | -------------------------------------------------------------------------------- /internal/node/janitor_handler.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "github.com/serverledge-faas/serverledge/internal/config" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type janitor struct { 10 | Interval time.Duration 11 | stop chan bool 12 | } 13 | 14 | var ( 15 | Instance *janitor 16 | ) 17 | 18 | var lock = &sync.Mutex{} 19 | 20 | // GetJanitorInstance : singleton implementation to retrieve THE container janitor 21 | func GetJanitorInstance() *janitor { 22 | lock.Lock() 23 | defer lock.Unlock() 24 | 25 | if Instance == nil { 26 | // todo adjust default interval 27 | Instance = runJanitor(time.Duration(config.GetInt(config.POOL_CLEANUP_PERIOD, 30)) * time.Second) // <-- thread safe 28 | } 29 | 30 | return Instance 31 | } 32 | 33 | func (j *janitor) run() { 34 | ticker := time.NewTicker(j.Interval) 35 | for { 36 | select { 37 | case <-ticker.C: 38 | DeleteExpiredContainer() 39 | case <-j.stop: 40 | ticker.Stop() 41 | return 42 | } 43 | } 44 | } 45 | 46 | func StopJanitor() { 47 | Instance.stop <- true 48 | } 49 | 50 | func runJanitor(ci time.Duration) *janitor { 51 | j := &janitor{ 52 | Interval: ci, 53 | stop: make(chan bool), 54 | } 55 | go j.run() 56 | return j 57 | } 58 | -------------------------------------------------------------------------------- /internal/node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | var OutOfResourcesErr = errors.New("not enough resources for function execution") 10 | 11 | var NodeIdentifier string 12 | 13 | type NodeResources struct { 14 | sync.RWMutex 15 | AvailableMemMB int64 16 | AvailableCPUs float64 17 | DropCount int64 18 | ContainerPools map[string]*ContainerPool 19 | } 20 | 21 | func (n *NodeResources) String() string { 22 | return fmt.Sprintf("[CPUs: %f - Mem: %d]", n.AvailableCPUs, n.AvailableMemMB) 23 | } 24 | 25 | var Resources NodeResources 26 | -------------------------------------------------------------------------------- /internal/registration/types.go: -------------------------------------------------------------------------------- 1 | package registration 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/LK4D4/trylock" 7 | "github.com/hexablock/vivaldi" 8 | ) 9 | 10 | var UnavailableClientErr = errors.New("etcd client unavailable") 11 | var IdRegistrationErr = errors.New("etcd error: could not complete the registration") 12 | var KeepAliveErr = errors.New(" The system can't renew your registration key") 13 | 14 | type Registry struct { 15 | Area string 16 | Key string 17 | Client *vivaldi.Client 18 | RwMtx trylock.Mutex 19 | NearbyServersMap map[string]*StatusInformation 20 | serversMap map[string]*StatusInformation 21 | etcdCh chan bool 22 | } 23 | 24 | type StatusInformation struct { 25 | Url string 26 | AvailableWarmContainers map[string]int // = 27 | AvailableMemMB int64 28 | AvailableCPUs float64 29 | Coordinates vivaldi.Coordinate 30 | LoadAvg []float64 31 | } 32 | -------------------------------------------------------------------------------- /internal/scheduling/async.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/serverledge-faas/serverledge/internal/function" 10 | "github.com/serverledge-faas/serverledge/utils" 11 | clientv3 "go.etcd.io/etcd/client/v3" 12 | ) 13 | 14 | func publishAsyncResponse(reqId string, response function.Response) { 15 | etcdClient, err := utils.GetEtcdClient() 16 | if err != nil { 17 | log.Fatal("Client not available") 18 | return 19 | } 20 | 21 | ctx := context.Background() 22 | 23 | resp, err := etcdClient.Grant(ctx, 1800) // 30 min 24 | if err != nil { 25 | log.Fatal(err) 26 | return 27 | } 28 | 29 | key := fmt.Sprintf("async/%s", reqId) 30 | payload, err := json.Marshal(response) 31 | if err != nil { 32 | log.Printf("Could not marshal response: %v\n", err) 33 | return 34 | } 35 | 36 | _, err = etcdClient.Put(ctx, key, string(payload), clientv3.WithLease(resp.ID)) 37 | if err != nil { 38 | log.Fatal(err) 39 | return 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/scheduling/cloudonly_policy.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | // CloudOnlyPolicy can be used on Edge nodes to always offload on cloud. If offloading is disabled, the request is dropped 4 | import "github.com/serverledge-faas/serverledge/internal/function" 5 | 6 | type CloudOnlyPolicy struct{} 7 | 8 | func (p *CloudOnlyPolicy) Init() { 9 | } 10 | 11 | func (p *CloudOnlyPolicy) OnCompletion(_ *function.Function, _ *function.ExecutionReport) { 12 | 13 | } 14 | 15 | func (p *CloudOnlyPolicy) OnArrival(r *scheduledRequest) { 16 | if r.CanDoOffloading { 17 | handleCloudOffload(r) 18 | } else { 19 | dropRequest(r) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/scheduling/edgeCloudPolicy.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import ( 4 | "github.com/serverledge-faas/serverledge/internal/function" 5 | "github.com/serverledge-faas/serverledge/internal/node" 6 | ) 7 | 8 | // CloudEdgePolicy supports only Edge-Cloud Offloading. Executes locally first, 9 | // but if no resources are available and offload is enabled offloads the request to a cloud node. 10 | // If no resources are available and offloading is disabled, drops the request. 11 | type CloudEdgePolicy struct{} 12 | 13 | func (p *CloudEdgePolicy) Init() { 14 | } 15 | 16 | func (p *CloudEdgePolicy) OnCompletion(_ *function.Function, _ *function.ExecutionReport) { 17 | 18 | } 19 | 20 | func (p *CloudEdgePolicy) OnArrival(r *scheduledRequest) { 21 | containerID, warm, err := node.AcquireContainer(r.Fun) 22 | if err == nil { 23 | execLocally(r, containerID, warm) 24 | } else if r.CanDoOffloading { 25 | handleCloudOffload(r) 26 | } else { 27 | dropRequest(r) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/scheduling/edgeOnlyPolicy.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import ( 4 | "github.com/serverledge-faas/serverledge/internal/function" 5 | "github.com/serverledge-faas/serverledge/internal/node" 6 | ) 7 | 8 | // EdgePolicy supports only Edge-Edge offloading. Always does offloading to an edge node if enabled. When offloading is not enabled executes the request locally. 9 | type EdgePolicy struct{} 10 | 11 | func (p *EdgePolicy) Init() { 12 | } 13 | 14 | func (p *EdgePolicy) OnCompletion(_ *function.Function, _ *function.ExecutionReport) { 15 | 16 | } 17 | 18 | func (p *EdgePolicy) OnArrival(r *scheduledRequest) { 19 | if r.CanDoOffloading { 20 | url := pickEdgeNodeForOffloading(r) 21 | if url != "" { 22 | handleOffload(r, url) 23 | return 24 | } 25 | } else { 26 | containerID, warm, err := node.AcquireContainer(r.Fun) 27 | if err == nil { 28 | execLocally(r, containerID, warm) 29 | return 30 | } 31 | } 32 | 33 | dropRequest(r) 34 | } 35 | -------------------------------------------------------------------------------- /internal/scheduling/execution.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/serverledge-faas/serverledge/internal/container" 9 | "github.com/serverledge-faas/serverledge/internal/executor" 10 | "github.com/serverledge-faas/serverledge/internal/function" 11 | ) 12 | 13 | const HANDLER_DIR = "/app" 14 | 15 | // Execute serves a request on the specified container. 16 | func Execute(cont *container.Container, r *scheduledRequest, isWarm bool) (function.ExecutionReport, error) { 17 | 18 | log.Printf("[%s] Executing on container: %v", r.Fun, cont.ID) 19 | 20 | var req executor.InvocationRequest 21 | if r.Fun.Runtime == container.CUSTOM_RUNTIME { 22 | req = executor.InvocationRequest{ 23 | Params: r.Params, 24 | ReturnOutput: r.ReturnOutput, 25 | } 26 | } else { 27 | cmd := container.RuntimeToInfo[r.Fun.Runtime].InvocationCmd 28 | req = executor.InvocationRequest{ 29 | Command: cmd, 30 | Params: r.Params, 31 | Handler: r.Fun.Handler, 32 | HandlerDir: HANDLER_DIR, 33 | ReturnOutput: r.ReturnOutput, 34 | } 35 | } 36 | 37 | t0 := time.Now() 38 | initTime := t0.Sub(r.Arrival).Seconds() 39 | 40 | response, invocationWait, err := container.Execute(cont.ID, &req) 41 | 42 | if err != nil { 43 | logs, errLog := container.GetLog(cont.ID) 44 | if errLog == nil { 45 | fmt.Println(logs) 46 | } else { 47 | fmt.Printf("Failed to get log: %v\n", errLog) 48 | } 49 | 50 | // notify scheduler 51 | completions <- &completionNotification{fun: r.Fun, cont: cont, executionReport: nil} 52 | return function.ExecutionReport{}, fmt.Errorf("[%s] Execution failed on container %v: %v ", r, cont.ID, err) 53 | } 54 | 55 | if !response.Success { 56 | // notify scheduler 57 | completions <- &completionNotification{fun: r.Fun, cont: cont, executionReport: nil} 58 | return function.ExecutionReport{}, fmt.Errorf("Function execution failed") 59 | } 60 | 61 | report := function.ExecutionReport{Result: response.Result, 62 | Output: response.Output, 63 | IsWarmStart: isWarm, 64 | Duration: time.Now().Sub(t0).Seconds() - invocationWait.Seconds(), 65 | ResponseTime: time.Now().Sub(r.Arrival).Seconds()} 66 | 67 | // initializing containers may require invocation retries, adding 68 | // latency 69 | report.InitTime = initTime + invocationWait.Seconds() 70 | 71 | // notify scheduler 72 | completions <- &completionNotification{fun: r.Fun, cont: cont, executionReport: &report} 73 | 74 | return report, nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/scheduling/offloading.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/serverledge-faas/serverledge/internal/client" 13 | "github.com/serverledge-faas/serverledge/internal/function" 14 | "github.com/serverledge-faas/serverledge/internal/node" 15 | "github.com/serverledge-faas/serverledge/internal/registration" 16 | ) 17 | 18 | const SCHED_ACTION_OFFLOAD = "O" 19 | 20 | func pickEdgeNodeForOffloading(r *scheduledRequest) (url string) { 21 | nearbyServersMap := registration.Reg.NearbyServersMap 22 | if nearbyServersMap == nil { 23 | return "" 24 | } 25 | //first, search for warm container 26 | for _, v := range nearbyServersMap { 27 | if v.AvailableWarmContainers[r.Fun.Name] != 0 && v.AvailableCPUs >= r.Request.Fun.CPUDemand { 28 | return v.Url 29 | } 30 | } 31 | //second, (nobody has warm container) search for available memory 32 | for _, v := range nearbyServersMap { 33 | if v.AvailableMemMB >= r.Request.Fun.MemoryMB && v.AvailableCPUs >= r.Request.Fun.CPUDemand { 34 | return v.Url 35 | } 36 | } 37 | return "" 38 | } 39 | 40 | func Offload(r *function.Request, serverUrl string) (function.ExecutionReport, error) { 41 | // Prepare request 42 | request := client.InvocationRequest{Params: r.Params, QoSClass: r.Class, QoSMaxRespT: r.MaxRespT} 43 | invocationBody, err := json.Marshal(request) 44 | if err != nil { 45 | log.Print(err) 46 | return function.ExecutionReport{}, err 47 | } 48 | sendingTime := time.Now() // used to compute latency later on 49 | resp, err := offloadingClient.Post(serverUrl+"/invoke/"+r.Fun.Name, "application/json", 50 | bytes.NewBuffer(invocationBody)) 51 | 52 | if err != nil { 53 | log.Print(err) 54 | return function.ExecutionReport{}, err 55 | } 56 | if resp.StatusCode != http.StatusOK { 57 | if resp.StatusCode == http.StatusTooManyRequests { 58 | return function.ExecutionReport{}, node.OutOfResourcesErr 59 | } 60 | return function.ExecutionReport{}, fmt.Errorf("Remote returned: %v", resp.StatusCode) 61 | } 62 | 63 | var response function.Response 64 | defer func(Body io.ReadCloser) { 65 | err := Body.Close() 66 | if err != nil { 67 | fmt.Printf("Error while closing offload response body: %s\n", err) 68 | } 69 | }(resp.Body) 70 | body, _ := io.ReadAll(resp.Body) 71 | if err = json.Unmarshal(body, &response); err != nil { 72 | return function.ExecutionReport{}, err 73 | } 74 | now := time.Now() 75 | 76 | execReport := &response.ExecutionReport 77 | execReport.ResponseTime = now.Sub(r.Arrival).Seconds() 78 | 79 | // TODO: check how this is used in the QoSAware policy 80 | // It was originially computed as "report.Arrival - sendingTime" 81 | execReport.OffloadLatency = now.Sub(sendingTime).Seconds() - execReport.Duration - execReport.InitTime 82 | execReport.SchedAction = SCHED_ACTION_OFFLOAD 83 | 84 | return response.ExecutionReport, nil 85 | } 86 | 87 | func OffloadAsync(r *function.Request, serverUrl string) error { 88 | // Prepare request 89 | request := client.InvocationRequest{Params: r.Params, 90 | QoSClass: r.Class, 91 | QoSMaxRespT: r.MaxRespT, 92 | Async: true} 93 | invocationBody, err := json.Marshal(request) 94 | if err != nil { 95 | log.Print(err) 96 | return err 97 | } 98 | resp, err := offloadingClient.Post(serverUrl+"/invoke/"+r.Fun.Name, "application/json", 99 | bytes.NewBuffer(invocationBody)) 100 | 101 | if err != nil { 102 | log.Print(err) 103 | return err 104 | } 105 | if resp.StatusCode != http.StatusOK { 106 | return fmt.Errorf("Remote returned: %v", resp.StatusCode) 107 | } 108 | 109 | // there is nothing to wait for 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /internal/scheduling/policy.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import "github.com/serverledge-faas/serverledge/internal/function" 4 | 5 | type Policy interface { 6 | Init() 7 | OnCompletion(fun *function.Function, executionReport *function.ExecutionReport) 8 | OnArrival(request *scheduledRequest) 9 | } 10 | -------------------------------------------------------------------------------- /internal/scheduling/policy_default.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import ( 4 | "errors" 5 | "github.com/serverledge-faas/serverledge/internal/function" 6 | "log" 7 | 8 | "github.com/serverledge-faas/serverledge/internal/config" 9 | "github.com/serverledge-faas/serverledge/internal/node" 10 | ) 11 | 12 | // DefaultLocalPolicy can be used on single node deployments. Directly executes the function locally, or drops the request if there aren't enough resources. 13 | type DefaultLocalPolicy struct { 14 | queue queue 15 | } 16 | 17 | func (p *DefaultLocalPolicy) Init() { 18 | queueCapacity := config.GetInt(config.SCHEDULER_QUEUE_CAPACITY, 0) 19 | if queueCapacity > 0 { 20 | log.Printf("Configured queue with capacity %d\n", queueCapacity) 21 | p.queue = NewFIFOQueue(queueCapacity) 22 | } else { 23 | p.queue = nil 24 | } 25 | } 26 | 27 | func (p *DefaultLocalPolicy) OnCompletion(_ *function.Function, _ *function.ExecutionReport) { 28 | if p.queue == nil { 29 | return 30 | } 31 | 32 | p.queue.Lock() 33 | defer p.queue.Unlock() 34 | if p.queue.Len() == 0 { 35 | return 36 | } 37 | 38 | req := p.queue.Front() 39 | 40 | containerID, err := node.AcquireWarmContainer(req.Fun) 41 | if err == nil { 42 | p.queue.Dequeue() 43 | log.Printf("[%s] Warm start from the queue (length=%d)\n", req, p.queue.Len()) 44 | execLocally(req, containerID, true) 45 | return 46 | } 47 | 48 | if errors.Is(err, node.NoWarmFoundErr) { 49 | if node.AcquireResources(req.Fun.CPUDemand, req.Fun.MemoryMB, true) { 50 | log.Printf("[%s] Cold start from the queue\n", req) 51 | p.queue.Dequeue() 52 | 53 | // This avoids blocking the thread during the cold 54 | // start, but also allows us to check for resource 55 | // availability before dequeueing 56 | go func() { 57 | newContainer, err := node.NewContainerWithAcquiredResources(req.Fun, false, false) 58 | if err != nil { 59 | dropRequest(req) 60 | } else { 61 | execLocally(req, newContainer, false) 62 | } 63 | }() 64 | return 65 | } 66 | } else if errors.Is(err, node.OutOfResourcesErr) { 67 | } else { 68 | // other error 69 | log.Printf("%v", err) 70 | p.queue.Dequeue() 71 | dropRequest(req) 72 | } 73 | } 74 | 75 | // OnArrival for default policy is executed every time a function is invoked, before invoking the function 76 | func (p *DefaultLocalPolicy) OnArrival(r *scheduledRequest) { 77 | containerID, warm, err := node.AcquireContainer(r.Fun) 78 | if err == nil { 79 | execLocally(r, containerID, warm) // decides to execute locally 80 | return 81 | } 82 | 83 | if errors.Is(err, node.OutOfResourcesErr) { 84 | // pass 85 | } else { 86 | // other error 87 | dropRequest(r) 88 | return 89 | } 90 | 91 | // enqueue if possible 92 | if p.queue != nil { 93 | p.queue.Lock() 94 | defer p.queue.Unlock() 95 | if p.queue.Enqueue(r) { 96 | log.Printf("[%s] Added to queue (length=%d)\n", r, p.queue.Len()) 97 | return 98 | } 99 | } 100 | 101 | dropRequest(r) 102 | } 103 | -------------------------------------------------------------------------------- /internal/scheduling/queue.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import "sync" 4 | 5 | type queue interface { 6 | Enqueue(r *scheduledRequest) bool 7 | Dequeue() *scheduledRequest 8 | Front() *scheduledRequest 9 | Len() int 10 | Lock() 11 | Unlock() 12 | } 13 | 14 | // FIFOQueue defines a circular queue 15 | type FIFOQueue struct { 16 | sync.Mutex 17 | data []*scheduledRequest 18 | capacity int 19 | head int 20 | tail int 21 | size int 22 | } 23 | 24 | // NewFIFOQueue creates a queue 25 | func NewFIFOQueue(n int) *FIFOQueue { 26 | if n < 1 { 27 | return nil 28 | } 29 | return &FIFOQueue{ 30 | data: make([]*scheduledRequest, n), 31 | capacity: n, 32 | head: 0, 33 | tail: 0, 34 | size: 0, 35 | } 36 | } 37 | 38 | // IsEmpty returns true if queue is empty 39 | func (q *FIFOQueue) IsEmpty() bool { 40 | return q != nil && q.size == 0 41 | } 42 | 43 | // IsFull returns true if queue is full 44 | func (q *FIFOQueue) IsFull() bool { 45 | return q.size == q.capacity 46 | } 47 | 48 | // Enqueue pushes an element to the back 49 | func (q *FIFOQueue) Enqueue(v *scheduledRequest) bool { 50 | if q.IsFull() { 51 | return false 52 | } 53 | 54 | q.data[q.tail] = v 55 | q.tail = (q.tail + 1) % q.capacity 56 | q.size = q.size + 1 57 | return true 58 | } 59 | 60 | // Dequeue fetches a element from queue 61 | func (q *FIFOQueue) Dequeue() *scheduledRequest { 62 | if q.IsEmpty() { 63 | return nil 64 | } 65 | v := q.data[q.head] 66 | q.head = (q.head + 1) % q.capacity 67 | q.size = q.size - 1 68 | return v 69 | } 70 | 71 | func (q *FIFOQueue) Front() *scheduledRequest { 72 | if q.IsEmpty() { 73 | return nil 74 | } 75 | v := q.data[q.head] 76 | return v 77 | } 78 | 79 | // Len returns the current length of the queue 80 | func (q *FIFOQueue) Len() int { 81 | return q.size 82 | } 83 | -------------------------------------------------------------------------------- /internal/scheduling/types.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | import ( 4 | "github.com/serverledge-faas/serverledge/internal/container" 5 | "github.com/serverledge-faas/serverledge/internal/function" 6 | ) 7 | 8 | // scheduledRequest represents a Request within the scheduling subsystem 9 | type scheduledRequest struct { 10 | *function.Request 11 | decisionChannel chan schedDecision 12 | } 13 | 14 | type completionNotification struct { 15 | fun *function.Function 16 | cont *container.Container 17 | executionReport *function.ExecutionReport 18 | } 19 | 20 | // schedDecision wraps a action made by the scheduler. 21 | // Possible decisions are 1) drop, 2) execute locally or 3) execute on a remote 22 | // Node (offloading). 23 | type schedDecision struct { 24 | action action 25 | cont *container.Container 26 | remoteHost string 27 | useWarm bool 28 | } 29 | 30 | type action int64 31 | 32 | const ( 33 | DROP action = 0 34 | EXEC_LOCAL = 1 35 | EXEC_REMOTE = 2 36 | ) 37 | -------------------------------------------------------------------------------- /internal/telemetry/otel.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os" 7 | 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 10 | "go.opentelemetry.io/otel/propagation" 11 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 12 | "go.opentelemetry.io/otel/trace" 13 | ) 14 | 15 | var DefaultTracer trace.Tracer = nil 16 | 17 | // setupOTelSDK bootstraps the OpenTelemetry pipeline. 18 | // If it does not return an error, make sure to call shutdown for proper cleanup. 19 | func SetupOTelSDK(ctx context.Context, outputFilename string) (shutdown func(context.Context) error, err error) { 20 | var shutdownFuncs []func(context.Context) error 21 | 22 | // shutdown calls cleanup functions registered via shutdownFuncs. 23 | // The errors from the calls are joined. 24 | // Each registered cleanup will be invoked once. 25 | shutdown = func(ctx context.Context) error { 26 | var err error 27 | for _, fn := range shutdownFuncs { 28 | err = errors.Join(err, fn(ctx)) 29 | } 30 | shutdownFuncs = nil 31 | return err 32 | } 33 | 34 | // handleErr calls shutdown for cleanup and makes sure that all errors are returned. 35 | handleErr := func(inErr error) { 36 | err = errors.Join(inErr, shutdown(ctx)) 37 | } 38 | 39 | // Set up propagator. 40 | prop := newPropagator() 41 | otel.SetTextMapPropagator(prop) 42 | 43 | // Set up trace provider. 44 | f, err := os.OpenFile(outputFilename, os.O_WRONLY|os.O_CREATE, 0644) 45 | if err != nil { 46 | handleErr(err) 47 | return 48 | } 49 | traceExporter, err := stdouttrace.New(stdouttrace.WithWriter(f)) 50 | if err != nil { 51 | handleErr(err) 52 | return 53 | } 54 | shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) error { 55 | return f.Close() 56 | }) 57 | 58 | tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithBatcher(traceExporter)) 59 | 60 | shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) 61 | otel.SetTracerProvider(tracerProvider) 62 | // Finally, set the tracer that can be used for this package. 63 | DefaultTracer = tracerProvider.Tracer("github.com/serverledge-faas/serverledge") 64 | 65 | // NOTE: could boostrap metric provider as well 66 | 67 | return 68 | } 69 | 70 | func newPropagator() propagation.TextMapPropagator { 71 | return propagation.NewCompositeTextMapPropagator( 72 | propagation.TraceContext{}, 73 | propagation.Baggage{}, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /internal/test/asl/bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "NonExistentKey": "I do not exist", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "Mistake": { 6 | "TrueMistake": "error", 7 | "ShouldNotWork": 123, 8 | "end": true 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /internal/test/asl/choice_boolexpr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "ChoiceState", 4 | "States": { 5 | "ChoiceState": { 6 | "Type": "Choice", 7 | "Choices": [ 8 | { 9 | "Not": { 10 | "Variable": "$.type", 11 | "StringEquals": "Private" 12 | }, 13 | "Next": "Public" 14 | }, 15 | { 16 | "And": [ 17 | { 18 | "Variable": "$.value", 19 | "IsPresent": true 20 | }, 21 | { 22 | "Variable": "$.value", 23 | "IsNumeric": true 24 | }, 25 | { 26 | "Variable": "$.value", 27 | "NumericGreaterThanEquals": 20 28 | }, 29 | { 30 | "Variable": "$.value", 31 | "NumericLessThan": 30 32 | } 33 | ], 34 | "Next": "ValueInTwenties" 35 | } 36 | ], 37 | "Default": "DefaultState" 38 | }, 39 | "Public": { 40 | "Comment": "Lang=Python", 41 | "Type": "Task", 42 | "Resource": "inc", 43 | "Next": "NextState" 44 | }, 45 | "ValueInTwenties": { 46 | "Comment": "Lang=Python", 47 | "Type": "Task", 48 | "Resource": "double", 49 | "Next": "NextState" 50 | }, 51 | 52 | "DefaultState": { 53 | "Comment": "Lang=Javascript", 54 | "Type": "Task", 55 | "Resource": "hello", 56 | "End": true 57 | }, 58 | 59 | "NextState": { 60 | "Comment": "Lang=Python", 61 | "Type": "Task", 62 | "Resource": "inc", 63 | "End": true 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /internal/test/asl/choice_datastring.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "ChoiceState", 4 | "States": { 5 | "ChoiceState": { 6 | "Type": "Choice", 7 | "Choices": [ 8 | { 9 | "Variable": "$.input", 10 | "StringEquals": "ludovico", 11 | "Next": "FirstMatchState" 12 | }, 13 | { 14 | "Variable": "$.input", 15 | "StringEquals": "valerio", 16 | "Next": "SecondMatchState" 17 | }, 18 | { 19 | "Variable": "$.input", 20 | "StringEquals": "rebecca", 21 | "Next": "ThirdMatchState" 22 | } 23 | ], 24 | "Default": "DefaultState" 25 | }, 26 | "FirstMatchState": { 27 | "Comment": "Lang=Python", 28 | "Type": "Task", 29 | "Resource": "hello", 30 | "Next": "NextState" 31 | }, 32 | "SecondMatchState": { 33 | "Comment": "Lang=Python", 34 | "Type": "Task", 35 | "Resource": "hello", 36 | "Next": "NextState" 37 | }, 38 | "ThirdMatchState": { 39 | "Comment": "Lang=Python", 40 | "Type": "Task", 41 | "Resource": "hello", 42 | "Next": "NextState" 43 | }, 44 | 45 | "DefaultState": { 46 | "Comment": "Lang=Python", 47 | "Type": "Task", 48 | "Resource": "hello", 49 | "End": true 50 | }, 51 | 52 | "NextState": { 53 | "Comment": "Lang=Python", 54 | "Type": "Task", 55 | "Resource": "hello", 56 | "End": true 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /internal/test/asl/choice_datatestexpr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "ChoiceState", 4 | "States": { 5 | "ChoiceState": { 6 | "Type": "Choice", 7 | "Choices": [ 8 | { 9 | "Variable": "$.input", 10 | "NumericEquals": 1, 11 | "Next": "FirstMatchState" 12 | }, 13 | { 14 | "Variable": "$.input", 15 | "NumericEquals": 2, 16 | "Next": "SecondMatchState" 17 | }, 18 | { 19 | "Variable": "$.input", 20 | "NumericEquals": 3, 21 | "Next": "ThirdMatchState" 22 | } 23 | ], 24 | "Default": "DefaultState" 25 | }, 26 | "FirstMatchState": { 27 | "Comment": "Lang=Python", 28 | "Type": "Task", 29 | "Resource": "inc", 30 | "Next": "NextState" 31 | }, 32 | "SecondMatchState": { 33 | "Comment": "Lang=Python", 34 | "Type": "Task", 35 | "Resource": "double", 36 | "Next": "NextState" 37 | }, 38 | "ThirdMatchState": { 39 | "Comment": "Lang=Python", 40 | "Type": "Task", 41 | "Resource": "inc", 42 | "Next": "NextState" 43 | }, 44 | 45 | "DefaultState": { 46 | "Comment": "Lang=Python", 47 | "Type": "Task", 48 | "Resource": "hello", 49 | "End": true 50 | }, 51 | 52 | "NextState": { 53 | "Comment": "Lang=Python", 54 | "Type": "Task", 55 | "Resource": "double", 56 | "End": true 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /internal/test/asl/choice_numeq_succeed_fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "FirstState": { 6 | "Comment": "Lang=Python", 7 | "Type": "Task", 8 | "Resource": "inc", 9 | "Next": "ChoiceState" 10 | }, 11 | "ChoiceState": { 12 | "Type": "Choice", 13 | "Choices": [{ 14 | "Variable": "$.input", 15 | "NumericEquals": 1, 16 | "Next": "FirstMatchState" 17 | }, 18 | { 19 | "Variable": "$.input", 20 | "NumericEquals": 2, 21 | "Next": "SucceedState" 22 | } 23 | ], 24 | "Default": "DefaultState" 25 | }, 26 | 27 | "FirstMatchState": { 28 | "Comment": "Lang=Python", 29 | "Type": "Task", 30 | "Resource": "inc", 31 | "Next": "NextState" 32 | }, 33 | 34 | "SucceedState": { 35 | "Type": "Succeed" 36 | }, 37 | 38 | "DefaultState": { 39 | "Type": "Fail", 40 | "Error": "DefaultStateError", 41 | "Cause": "No Matches!" 42 | }, 43 | 44 | "NextState": { 45 | "Comment": "Lang=Python", 46 | "Type": "Task", 47 | "Resource": "double", 48 | "End": true 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /internal/test/asl/machine.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "FirstState": { 6 | "Type": "Task", 7 | "Resource": "arn:aws:lambda:region-1:1234567890:function:FUNCTION_NAME", 8 | "Next": "ChoiceState" 9 | }, 10 | "ChoiceState": { 11 | "Type": "Choice", 12 | "Choices": [{ 13 | "Variable": "$.foo", 14 | "NumericEquals": 1, 15 | "Next": "FirstMatchState" 16 | }, 17 | { 18 | "Variable": "$.foo", 19 | "NumericEquals": 2, 20 | "Next": "SecondMatchState" 21 | } 22 | ], 23 | "Default": "DefaultState" 24 | }, 25 | 26 | "FirstMatchState": { 27 | "Type": "Task", 28 | "Resource": "arn:aws:lambda:region-1:1234567890:function:OnFirstMatch", 29 | "Next": "NextState" 30 | }, 31 | 32 | "SecondMatchState": { 33 | "Type": "Task", 34 | "Resource": "arn:aws:lambda:region-1:1234567890:function:OnSecondMatch", 35 | "Next": "NextState" 36 | }, 37 | 38 | "DefaultState": { 39 | "Type": "Fail", 40 | "Error": "DefaultStateError", 41 | "Cause": "No Matches!" 42 | }, 43 | 44 | "NextState": { 45 | "Type": "Task", 46 | "Resource": "arn:aws:lambda:region-1:1234567890:function:FUNCTION_NAME", 47 | "End": true 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /internal/test/asl/mixed_sequence.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "A sequence, but individual tasks are mixed up", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "SecondState": { 6 | "Type": "Task", 7 | "Resource": "double", 8 | "Next": "ThirdState" 9 | }, 10 | "FourthState": { 11 | "Type": "Task", 12 | "Resource": "double", 13 | "Next": "FifthState" 14 | }, 15 | "FifthState": { 16 | "Type": "Task", 17 | "Resource": "inc", 18 | "End": true 19 | }, 20 | "FirstState": { 21 | "Type": "Task", 22 | "Resource": "inc", 23 | "Next": "SecondState" 24 | }, 25 | "ThirdState": { 26 | "Type": "Task", 27 | "Resource": "inc", 28 | "Next": "FourthState" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /internal/test/asl/sequence.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "FirstState": { 6 | "Type": "Task", 7 | "Resource": "noop", 8 | "Next": "SecondState" 9 | }, 10 | "SecondState": { 11 | "Type": "Task", 12 | "Resource": "noop", 13 | "Next": "ThirdState" 14 | }, 15 | "ThirdState": { 16 | "Type": "Task", 17 | "Resource": "noop", 18 | "Next": "FourthState" 19 | }, 20 | "FourthState": { 21 | "Type": "Task", 22 | "Resource": "noop", 23 | "Next": "FifthState" 24 | }, 25 | "FifthState": { 26 | "Type": "Task", 27 | "Resource": "noop", 28 | "End": true 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /internal/test/asl/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "A simple state machine with 2 task nodes", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "FirstState": { 6 | "Comment": "The first task", 7 | "Type": "Task", 8 | "Resource": "hello", 9 | "Next": "SecondState" 10 | }, 11 | "SecondState": { 12 | "Comment": "The second task", 13 | "Type": "Task", 14 | "Resource": "hello", 15 | "Next": "Final" 16 | }, 17 | "Final": { 18 | "Comment": "The end task", 19 | "Type": "Task", 20 | "Resource": "hello", 21 | "End": true 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /internal/test/asl/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "FirstState": { 6 | "Comment": "Lang=Python", 7 | "Type": "Task", 8 | "Resource": "inc", 9 | "Next": "ChoiceState" 10 | }, 11 | "ChoiceState": { 12 | "Type": "Choice", 13 | "Choices": [{ 14 | "Variable": "$.input", 15 | "NumericEquals": 1, 16 | "Next": "FirstMatchState" 17 | }, 18 | { 19 | "Variable": "$.input", 20 | "NumericEquals": 2, 21 | "Next": "SecondMatchState" 22 | } 23 | ], 24 | "Default": "DefaultState" 25 | }, 26 | 27 | "FirstMatchState": { 28 | "Comment": "Lang=Python", 29 | "Type": "Task", 30 | "Resource": "inc", 31 | "Next": "ChoiceState2" 32 | }, 33 | 34 | "SecondMatchState": { 35 | "Comment": "Lang=Python", 36 | "Type": "Task", 37 | "Resource": "double", 38 | "Next": "ChoiceState2" 39 | }, 40 | 41 | "ChoiceState2": { 42 | "Type": "Choice", 43 | "Choices": [{ 44 | "Variable": "$.input", 45 | "NumericEquals": 2, 46 | "Next": "NextState" 47 | }, 48 | { 49 | "Variable": "$.input", 50 | "NumericEquals": 4, 51 | "Next": "NextState" 52 | } 53 | ], 54 | "Default": "DefaultState2" 55 | }, 56 | 57 | "SucceedState": { 58 | "Type": "Succeed" 59 | }, 60 | 61 | "DefaultState": { 62 | "Type": "Fail", 63 | "Error": "DefaultStateError", 64 | "Cause": "No Matches!" 65 | }, 66 | 67 | "DefaultState2": { 68 | "Comment": "Lang=Python", 69 | "Type": "Task", 70 | "Resource": "inc", 71 | "End": true 72 | }, 73 | 74 | "NextState": { 75 | "Comment": "Lang=Python", 76 | "Type": "Task", 77 | "Resource": "double", 78 | "Next": "NextState2" 79 | }, 80 | 81 | "NextState2": { 82 | "Comment": "Lang=Python", 83 | "Type": "Task", 84 | "Resource": "inc", 85 | "End": true 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /internal/test/asl/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "FirstState": { 6 | "Comment": "Lang=Python", 7 | "Type": "Task", 8 | "Resource": "inc", 9 | "Next": "ChoiceState" 10 | }, 11 | "ChoiceState": { 12 | "Type": "Choice", 13 | "Choices": [{ 14 | "Variable": "$.input", 15 | "NumericEquals": 1, 16 | "Next": "ChoiceState2" 17 | }, 18 | { 19 | "Variable": "$.input", 20 | "NumericEquals": 2, 21 | "Next": "SucceedState" 22 | } 23 | ], 24 | "Default": "DefaultState" 25 | }, 26 | 27 | "ChoiceState2": { 28 | "Type": "Choice", 29 | "Choices": [{ 30 | "Variable": "$.input", 31 | "NumericEquals": 1, 32 | "Next": "NextState" 33 | } 34 | ], 35 | "Default": "DefaultState2" 36 | }, 37 | 38 | "SucceedState": { 39 | "Type": "Succeed" 40 | }, 41 | 42 | "DefaultState": { 43 | "Type": "Fail", 44 | "Error": "DefaultStateError", 45 | "Cause": "No Matches!" 46 | }, 47 | 48 | "DefaultState2": { 49 | "Comment": "Lang=Python", 50 | "Type": "Task", 51 | "Resource": "double", 52 | "End": true 53 | }, 54 | 55 | "NextState": { 56 | "Comment": "Lang=Python", 57 | "Type": "Task", 58 | "Resource": "inc", 59 | "Next": "NextState2" 60 | }, 61 | 62 | "NextState2": { 63 | "Comment": "Lang=Python", 64 | "Type": "Task", 65 | "Resource": "double", 66 | "End": true 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /internal/test/asl/test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "An example of the Amazon States Language using a choice state.", 3 | "StartAt": "FirstState", 4 | "States": { 5 | "FirstState": { 6 | "Comment": "Lang=Python", 7 | "Type": "Task", 8 | "Resource": "inc", 9 | "Next": "ChoiceState" 10 | }, 11 | "ChoiceState": { 12 | "Type": "Choice", 13 | "Choices": [{ 14 | "Variable": "$.input", 15 | "NumericEquals": 1, 16 | "Next": "FirstMatchState" 17 | }, 18 | { 19 | "Variable": "$.input", 20 | "NumericEquals": 2, 21 | "Next": "SecondMatchState" 22 | } 23 | ], 24 | "Default": "DefaultState" 25 | }, 26 | 27 | "FirstMatchState": { 28 | "Comment": "Lang=Python", 29 | "Type": "Task", 30 | "Resource": "inc", 31 | "Next": "ChoiceState2" 32 | }, 33 | 34 | "SecondMatchState": { 35 | "Comment": "Lang=Python", 36 | "Type": "Task", 37 | "Resource": "double", 38 | "Next": "ChoiceState3" 39 | }, 40 | 41 | "ChoiceState2": { 42 | "Type": "Choice", 43 | "Choices": [{ 44 | "Variable": "$.input", 45 | "NumericEquals": 2, 46 | "Next": "NextState" 47 | } 48 | 49 | ], 50 | "Default": "DefaultState2" 51 | }, 52 | 53 | "ChoiceState3": { 54 | "Type": "Choice", 55 | "Choices": [{ 56 | "Variable": "$.input", 57 | "NumericEquals": 4, 58 | "Next": "NextState" 59 | } 60 | ], 61 | "Default": "DefaultState2" 62 | }, 63 | 64 | "SucceedState": { 65 | "Type": "Succeed" 66 | }, 67 | 68 | "DefaultState": { 69 | "Type": "Fail", 70 | "Error": "DefaultStateError", 71 | "Cause": "No Matches!" 72 | }, 73 | 74 | "DefaultState2": { 75 | "Comment": "Lang=Python", 76 | "Type": "Task", 77 | "Resource": "inc", 78 | "End": true 79 | }, 80 | 81 | "NextState": { 82 | "Comment": "Lang=Python", 83 | "Type": "Task", 84 | "Resource": "double", 85 | "Next": "NextState2" 86 | }, 87 | 88 | "NextState2": { 89 | "Comment": "Lang=Python", 90 | "Type": "Task", 91 | "Resource": "double", 92 | "End": true 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /internal/test/condition_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/serverledge-faas/serverledge/internal/workflow" 8 | "github.com/serverledge-faas/serverledge/utils" 9 | ) 10 | 11 | var predicate1 = workflow.Predicate{Root: workflow.Condition{Type: workflow.And, Find: []bool{false, false}, Sub: []workflow.Condition{{Type: workflow.Eq, Op: []interface{}{2, 2}, Find: []bool{false, false}}, {Type: workflow.Greater, Op: []interface{}{4, 2}, Find: []bool{false, false}}}}} 12 | var predicate2 = workflow.Predicate{Root: workflow.Condition{Type: workflow.Or, Find: []bool{false, false}, Sub: []workflow.Condition{{Type: workflow.Const, Op: []interface{}{true}, Find: []bool{false}}, {Type: workflow.Smaller, Op: []interface{}{4, 2}, Find: []bool{false, false}}}}} 13 | var predicate3 = workflow.Predicate{Root: workflow.Condition{Type: workflow.Or, Find: []bool{false, false}, Sub: []workflow.Condition{predicate1.Root, {Type: workflow.Smaller, Op: []interface{}{4, 2}, Find: []bool{false, false}}}}} 14 | var predicate4 = workflow.Predicate{Root: workflow.Condition{Type: workflow.Not, Find: []bool{false}, Sub: []workflow.Condition{{Type: workflow.IsEmpty, Op: []interface{}{1, 2, 3, 4}, Find: []bool{false}}}}} 15 | 16 | func TestPredicateMarshal(t *testing.T) { 17 | 18 | predicates := []workflow.Predicate{predicate1, predicate2, predicate3, predicate4} 19 | for _, predicate := range predicates { 20 | val, err := json.Marshal(predicate) 21 | utils.AssertNil(t, err) 22 | 23 | var predicateTest workflow.Predicate 24 | errUnmarshal := json.Unmarshal(val, &predicateTest) 25 | utils.AssertNil(t, errUnmarshal) 26 | utils.AssertTrue(t, predicate.Equals(predicateTest)) 27 | } 28 | } 29 | 30 | func TestPredicate(t *testing.T) { 31 | ok := predicate1.Test(nil) 32 | utils.AssertTrue(t, ok) 33 | 34 | ok2 := predicate2.Test(nil) 35 | utils.AssertTrue(t, ok2) 36 | 37 | ok3 := predicate3.Test(nil) 38 | utils.AssertTrue(t, ok3) 39 | 40 | ok4 := predicate4.Test(nil) 41 | utils.AssertTrue(t, ok4) 42 | } 43 | 44 | func TestPrintPredicate(t *testing.T) { 45 | str := predicate1.LogicString() 46 | utils.AssertEquals(t, "(2 == 2 && 4 > 2)", str) 47 | 48 | str2 := predicate2.LogicString() 49 | utils.AssertEquals(t, "(true || 4 < 2)", str2) 50 | 51 | str3 := predicate3.LogicString() 52 | utils.AssertEquals(t, "((2 == 2 && 4 > 2) || 4 < 2)", str3) 53 | 54 | str4 := predicate4.LogicString() 55 | utils.AssertEquals(t, "!(IsEmpty(1))", str4) 56 | } 57 | 58 | func TestBuilder(t *testing.T) { 59 | built1 := workflow.NewPredicate().And( 60 | workflow.NewEqCondition(2, 2), 61 | workflow.NewGreaterCondition(4, 2), 62 | ).Build() 63 | 64 | utils.AssertTrue(t, built1.Equals(predicate1.Root)) 65 | 66 | built2 := workflow.NewPredicate().Or( 67 | workflow.NewConstCondition(true), 68 | workflow.NewSmallerCondition(4, 2), 69 | ).Build() 70 | 71 | utils.AssertTrue(t, built2.Equals(predicate2.Root)) 72 | 73 | built3 := workflow.NewPredicate().Or( 74 | workflow.NewAnd( 75 | workflow.NewEqCondition(2, 2), 76 | workflow.NewGreaterCondition(4, 2), 77 | ), 78 | workflow.NewSmallerCondition(4, 2), 79 | ).Build() 80 | utils.AssertTrue(t, built3.Equals(predicate3.Root)) 81 | 82 | built4 := workflow.NewPredicate().Not( 83 | workflow.NewEmptyCondition([]interface{}{1, 2, 3, 4}), 84 | ).Build() 85 | 86 | utils.AssertTrue(t, built4.Equals(predicate4.Root)) 87 | 88 | } 89 | -------------------------------------------------------------------------------- /internal/test/types_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/serverledge-faas/serverledge/internal/function" 5 | u "github.com/serverledge-faas/serverledge/utils" 6 | 7 | "testing" 8 | ) 9 | 10 | // DataTypeEnum Text test 11 | func TestText(t *testing.T) { 12 | tt := function.Text{} 13 | 14 | err := tt.TypeCheck("text") 15 | u.AssertNil(t, err) 16 | 17 | err2 := tt.TypeCheck(5) 18 | u.AssertNonNil(t, err2) 19 | } 20 | 21 | // DataTypeEnum Int test 22 | func TestInt(t *testing.T) { 23 | // Int represent an int value 24 | i := function.Int{} 25 | 26 | err := i.TypeCheck(4) 27 | u.AssertNil(t, err) 28 | 29 | err2 := i.TypeCheck("5") 30 | u.AssertNil(t, err2) 31 | 32 | err3 := i.TypeCheck("0103.1") 33 | u.AssertNonNil(t, err3) 34 | 35 | } 36 | 37 | // DataTypeEnum Float test 38 | func TestFloat(t *testing.T) { 39 | f := function.Float{} 40 | 41 | err := f.TypeCheck(2.5) 42 | u.AssertNil(t, err) 43 | 44 | err2 := f.TypeCheck("2.5") 45 | u.AssertNil(t, err2) 46 | 47 | err3 := f.TypeCheck(1) 48 | u.AssertNil(t, err3) 49 | 50 | err4 := f.TypeCheck("pizza") 51 | u.AssertNonNil(t, err4) 52 | 53 | } 54 | 55 | func TestBoolean(t *testing.T) { 56 | f := function.Bool{} 57 | 58 | err := f.TypeCheck(true) 59 | u.AssertNil(t, err) 60 | 61 | err2 := f.TypeCheck("false") 62 | u.AssertNil(t, err2) 63 | 64 | err3 := f.TypeCheck(1) 65 | u.AssertNil(t, err3) 66 | 67 | err4 := f.TypeCheck("0") 68 | u.AssertNil(t, err4) 69 | 70 | err5 := f.TypeCheck("fake") 71 | u.AssertNonNil(t, err5) 72 | 73 | } 74 | 75 | // DataTypeEnum Array test 76 | func TestArray(t *testing.T) { 77 | a := function.Array[function.Int]{} 78 | 79 | err := a.TypeCheck([]int{1, 2, 3, 4}) 80 | u.AssertNil(t, err) 81 | 82 | err2 := a.TypeCheck([]int{}) 83 | u.AssertNil(t, err2) 84 | // if the slice is empty, we do not care of the type 85 | err3 := a.TypeCheck([]string{}) 86 | u.AssertNil(t, err3) 87 | 88 | err4 := a.TypeCheck([]string{"a", "b", "c"}) 89 | u.AssertNonNil(t, err4) 90 | 91 | err5 := a.TypeCheck(999) 92 | u.AssertNil(t, err5) 93 | } 94 | -------------------------------------------------------------------------------- /internal/types/comparable.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // TODO: move out of this package 4 | type Comparable interface { 5 | Equals(cmp Comparable) bool 6 | } 7 | -------------------------------------------------------------------------------- /internal/workflow/async.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/serverledge-faas/serverledge/utils" 8 | clientv3 "go.etcd.io/etcd/client/v3" 9 | "log" 10 | ) 11 | 12 | func PublishAsyncInvocationResponse(reqId string, response InvocationResponse) { 13 | etcdClient, err := utils.GetEtcdClient() 14 | if err != nil { 15 | log.Fatal("Client not available") 16 | return 17 | } 18 | 19 | ctx := context.Background() 20 | 21 | resp, err := etcdClient.Grant(ctx, 1800) 22 | if err != nil { 23 | log.Fatal(err) 24 | return 25 | } 26 | 27 | key := fmt.Sprintf("async/%s", reqId) // async is for function and workflows, so we can reuse poll!!! 28 | payload, err := json.Marshal(response) 29 | if err != nil { 30 | log.Printf("Could not marshal response: %v", err) 31 | return 32 | } 33 | 34 | _, err = etcdClient.Put(ctx, key, string(payload), clientv3.WithLease(resp.ID)) 35 | if err != nil { 36 | log.Fatal(err) 37 | return 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/workflow/end_task.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lithammer/shortuuid" 7 | "github.com/serverledge-faas/serverledge/internal/types" 8 | ) 9 | 10 | // EndTask is a Task that represents the end of the Workflow. 11 | type EndTask struct { 12 | Id TaskId 13 | Type TaskType 14 | Result map[string]interface{} 15 | } 16 | 17 | func NewEndTask() *EndTask { 18 | return &EndTask{ 19 | Id: TaskId(shortuuid.New()), 20 | Type: End, 21 | Result: make(map[string]interface{}), 22 | } 23 | } 24 | 25 | func (e *EndTask) Equals(cmp types.Comparable) bool { 26 | e2, ok := cmp.(*EndTask) 27 | if !ok { 28 | return false 29 | } 30 | 31 | if len(e.Result) != len(e2.Result) { 32 | return false 33 | } 34 | 35 | for k := range e.Result { 36 | if e.Result[k] != e2.Result[k] { 37 | return false 38 | } 39 | } 40 | 41 | return e.Id == e2.Id && e.Type == e2.Type 42 | } 43 | 44 | func (e *EndTask) execute(progress *Progress, partialData *PartialData) (*PartialData, *Progress, bool, error) { 45 | progress.Complete(e.Id) 46 | return partialData, progress, false, nil // false because we want to stop when reaching the end 47 | } 48 | 49 | func (e *EndTask) AddOutput(workflow *Workflow, taskId TaskId) error { 50 | return nil // should not do anything. End node cannot be chained to anything 51 | } 52 | 53 | func (e *EndTask) GetNext() []TaskId { 54 | // we return an empty array, because this is the EndTask 55 | return make([]TaskId, 0) 56 | } 57 | 58 | func (e *EndTask) Width() int { 59 | return 1 60 | } 61 | 62 | func (e *EndTask) Name() string { 63 | return " End " 64 | } 65 | 66 | func (e *EndTask) String() string { 67 | return fmt.Sprintf("[EndTask]") 68 | } 69 | func (e *EndTask) setBranchId(number int) { 70 | } 71 | func (e *EndTask) GetBranchId() int { 72 | return 0 73 | } 74 | 75 | func (e *EndTask) GetId() TaskId { 76 | return e.Id 77 | } 78 | 79 | func (e *EndTask) GetType() TaskType { 80 | return e.Type 81 | } 82 | -------------------------------------------------------------------------------- /internal/workflow/fail_task.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lithammer/shortuuid" 6 | "github.com/serverledge-faas/serverledge/internal/types" 7 | ) 8 | 9 | type FailureTask struct { 10 | Id TaskId 11 | Type TaskType 12 | Error string 13 | Cause string 14 | 15 | OutputTo TaskId 16 | } 17 | 18 | func NewFailureTask(error, cause string) *FailureTask { 19 | if len(error) > 20 { 20 | fmt.Printf("error string identifier should be less than 20 characters but is %d characters long\n", len(error)) 21 | } 22 | fail := FailureTask{ 23 | Id: TaskId(shortuuid.New()), 24 | Type: Fail, 25 | Error: error, 26 | Cause: cause, 27 | } 28 | return &fail 29 | } 30 | 31 | func (f *FailureTask) Equals(cmp types.Comparable) bool { 32 | f2, ok := cmp.(*FailureTask) 33 | if !ok { 34 | return false 35 | } 36 | return f.Id == f2.Id && 37 | f.Type == f2.Type && 38 | f.Error == f2.Error && 39 | f.Cause == f2.Cause && 40 | f.OutputTo == f2.OutputTo 41 | } 42 | 43 | func (f *FailureTask) execute(progress *Progress, r *Request) (*PartialData, *Progress, bool, error) { 44 | 45 | output := make(map[string]interface{}) 46 | output[f.Error] = f.Cause 47 | outputData := NewPartialData(ReqId(r.Id), f.GetNext()[0], f.GetId(), output) 48 | 49 | progress.Complete(f.GetId()) 50 | 51 | shouldContinueExecution := f.GetType() != Fail && f.GetType() != Succeed 52 | return outputData, progress, shouldContinueExecution, nil 53 | } 54 | 55 | func (f *FailureTask) AddOutput(workflow *Workflow, taskId TaskId) error { 56 | _, ok := workflow.Tasks[taskId].(*EndTask) 57 | if !ok { 58 | return fmt.Errorf("the Fail can only be chained to an end task") 59 | } 60 | f.OutputTo = taskId 61 | return nil 62 | } 63 | 64 | func (f *FailureTask) GetNext() []TaskId { 65 | return []TaskId{f.OutputTo} 66 | } 67 | 68 | func (f *FailureTask) Width() int { 69 | return 1 70 | } 71 | 72 | func (f *FailureTask) Name() string { 73 | return " Fail " 74 | } 75 | 76 | func (f *FailureTask) String() string { 77 | return fmt.Sprintf("[Fail: %s]", f.Error) 78 | } 79 | 80 | func (f *FailureTask) GetId() TaskId { 81 | return f.Id 82 | } 83 | 84 | func (f *FailureTask) GetType() TaskType { 85 | return f.Type 86 | } 87 | -------------------------------------------------------------------------------- /internal/workflow/fanin_task.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lithammer/shortuuid" 6 | "github.com/serverledge-faas/serverledge/internal/types" 7 | ) 8 | 9 | // FanInTask receives and merges multiple input and produces a single result 10 | type FanInTask struct { 11 | Id TaskId 12 | Type TaskType 13 | OutputTo TaskId 14 | FanInDegree int 15 | } 16 | 17 | func NewFanInTask(fanInDegree int) *FanInTask { 18 | fanIn := FanInTask{ 19 | Id: TaskId(shortuuid.New()), 20 | Type: FanIn, 21 | OutputTo: "", 22 | FanInDegree: fanInDegree, 23 | } 24 | 25 | return &fanIn 26 | } 27 | 28 | func (f *FanInTask) execute(progress *Progress, input *PartialData, r *Request) (*PartialData, *Progress, bool, error) { 29 | outputData := NewPartialData(ReqId(r.Id), f.GetNext()[0], f.GetId(), input.Data) 30 | progress.Complete(f.GetId()) 31 | err := progress.AddReadyTask(f.GetNext()[0]) 32 | if err != nil { 33 | return nil, progress, false, err 34 | } 35 | 36 | return outputData, progress, true, nil 37 | } 38 | 39 | func (f *FanInTask) Equals(cmp types.Comparable) bool { 40 | switch f1 := cmp.(type) { 41 | case *FanInTask: 42 | return f.Id == f1.Id && f.FanInDegree == f1.FanInDegree && f.OutputTo == f1.OutputTo 43 | default: 44 | return false 45 | } 46 | } 47 | 48 | func (f *FanInTask) AddOutput(workflow *Workflow, taskId TaskId) error { 49 | f.OutputTo = taskId 50 | return nil 51 | } 52 | 53 | func (f *FanInTask) GetNext() []TaskId { 54 | // we only have one output 55 | return []TaskId{f.OutputTo} 56 | } 57 | 58 | func (f *FanInTask) Width() int { 59 | return f.FanInDegree 60 | } 61 | 62 | func (f *FanInTask) Name() string { 63 | return "Fan In" 64 | } 65 | 66 | func (f *FanInTask) String() string { 67 | return fmt.Sprintf("[FanInTask(%d)]", f.FanInDegree) 68 | } 69 | 70 | func (f *FanInTask) GetId() TaskId { 71 | return f.Id 72 | } 73 | 74 | func (f *FanInTask) GetType() TaskType { 75 | return f.Type 76 | } 77 | -------------------------------------------------------------------------------- /internal/workflow/pass_task.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lithammer/shortuuid" 6 | "github.com/serverledge-faas/serverledge/internal/types" 7 | ) 8 | 9 | type PassTask struct { 10 | Id TaskId 11 | Type TaskType 12 | Result string 13 | ResultPath string 14 | OutputTo TaskId 15 | } 16 | 17 | func NewPassTask(result string) *PassTask { 18 | passTask := PassTask{ 19 | Id: TaskId(shortuuid.New()), 20 | Type: Pass, 21 | Result: result, 22 | } 23 | return &passTask 24 | } 25 | 26 | func (p *PassTask) Equals(cmp types.Comparable) bool { 27 | p2, ok := cmp.(*PassTask) 28 | if !ok { 29 | return false 30 | } 31 | return p.Id == p2.Id && 32 | p.Type == p2.Type && 33 | p.Result == p2.Result && 34 | p.ResultPath == p2.ResultPath && 35 | p.OutputTo == p2.OutputTo 36 | } 37 | 38 | // AddOutput for a PassTask connects it to another Task, except StartTask 39 | func (p *PassTask) AddOutput(workflow *Workflow, taskId TaskId) error { 40 | _, ok := workflow.Tasks[taskId].(*StartTask) 41 | if ok { 42 | return fmt.Errorf("the PassTask cannot be chained to a startTask") 43 | } 44 | p.OutputTo = taskId 45 | return nil 46 | } 47 | 48 | func (p *PassTask) GetNext() []TaskId { 49 | return []TaskId{p.OutputTo} 50 | } 51 | 52 | func (p *PassTask) Width() int { 53 | return 1 54 | } 55 | 56 | func (p *PassTask) Name() string { 57 | return "Pass" 58 | } 59 | 60 | func (p *PassTask) String() string { 61 | return "[ Pass ]" 62 | } 63 | 64 | func (p *PassTask) GetId() TaskId { 65 | return p.Id 66 | } 67 | 68 | func (p *PassTask) GetType() TaskType { 69 | return p.Type 70 | } 71 | -------------------------------------------------------------------------------- /internal/workflow/report.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cornelk/hashmap" 6 | "github.com/serverledge-faas/serverledge/internal/function" 7 | ) 8 | 9 | type ExecutionReportId string 10 | 11 | func CreateExecutionReportId(task Task) ExecutionReportId { 12 | return ExecutionReportId(printType(task.GetType()) + "_" + string(task.GetId())) 13 | } 14 | 15 | type ExecutionReport struct { 16 | Result map[string]interface{} 17 | Reports *hashmap.Map[ExecutionReportId, *function.ExecutionReport] 18 | ResponseTime float64 // time waited by the user to get the output of the entire workflow 19 | Progress *Progress `json:"-"` // skipped in Json marshaling 20 | } 21 | 22 | func (cer *ExecutionReport) String() string { 23 | str := "[" 24 | str += fmt.Sprintf("\n\tResponseTime: %f,", cer.ResponseTime) 25 | str += "\n\tReports: [" 26 | if cer.Reports.Len() > 0 { 27 | j := 0 28 | cer.Reports.Range(func(id ExecutionReportId, report *function.ExecutionReport) bool { 29 | schedAction := "''" 30 | if report.SchedAction != "" { 31 | schedAction = report.SchedAction 32 | } 33 | output := "''" 34 | if report.Output != "" { 35 | output = report.Output 36 | } 37 | 38 | str += fmt.Sprintf("\n\t\t%s: {ResponseTime: %f, IsWarmStart: %v, InitTime: %f, OffloadLatency: %f, Duration: %f, SchedAction: %v, Output: %s, Result: %s}", id, report.ResponseTime, report.IsWarmStart, report.InitTime, report.OffloadLatency, report.Duration, schedAction, output, report.Result) 39 | if j < cer.Reports.Len()-1 { 40 | str += "," 41 | } 42 | if j == cer.Reports.Len()-1 { 43 | str += "\n\t]" 44 | } 45 | j++ 46 | return true 47 | }) 48 | } 49 | 50 | str += "\n\tResult: {" 51 | i := 0 52 | lll := len(cer.Result) 53 | for s, v := range cer.Result { 54 | if i == 0 { 55 | str += "\n" 56 | } 57 | str += fmt.Sprintf("\t\t%s: %v,", s, v) 58 | if i < lll-1 { 59 | str += ",\n" 60 | } else if i == lll-1 { 61 | str += "\n" 62 | } 63 | i++ 64 | } 65 | str += "\t}\n}\n" 66 | return str 67 | } 68 | -------------------------------------------------------------------------------- /internal/workflow/request.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/cornelk/hashmap" 7 | "github.com/serverledge-faas/serverledge/internal/function" 8 | ) 9 | 10 | type ReqId string 11 | 12 | // Request represents a workflow invocation, with params and metrics data 13 | type Request struct { 14 | Id string 15 | W *Workflow 16 | Params map[string]interface{} 17 | Arrival time.Time 18 | ExecReport ExecutionReport // each function has its execution report, and the workflow has additional metrics 19 | QoS function.RequestQoS // every function should have its QoS 20 | CanDoOffloading bool // every function inherits this flag 21 | Async bool 22 | } 23 | 24 | func NewRequest(reqId string, workflow *Workflow, params map[string]interface{}) *Request { 25 | return &Request{ 26 | Id: reqId, 27 | W: workflow, 28 | Params: params, 29 | Arrival: time.Now(), 30 | ExecReport: ExecutionReport{ 31 | Reports: hashmap.New[ExecutionReportId, *function.ExecutionReport](), // make(map[ExecutionReportId]*function.ExecutionReport), 32 | }, 33 | CanDoOffloading: true, 34 | Async: false, 35 | } 36 | } 37 | 38 | type InvocationResponse struct { 39 | Success bool 40 | Result map[string]interface{} 41 | Reports map[string]*function.ExecutionReport 42 | ResponseTime float64 // time waited by the user to get the output of the entire workflow (in seconds) 43 | } 44 | 45 | type AsyncInvocationResponse struct { 46 | ReqId string 47 | } 48 | -------------------------------------------------------------------------------- /internal/workflow/scheduler.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/serverledge-faas/serverledge/internal/function" 8 | ) 9 | 10 | func SubmitWorkflowInvocationRequest(req *Request) error { 11 | executionReport, err := req.W.Invoke(req) 12 | if err != nil { 13 | return err 14 | } 15 | req.ExecReport = executionReport 16 | req.ExecReport.ResponseTime = time.Now().Sub(req.Arrival).Seconds() 17 | return nil 18 | } 19 | 20 | // TODO: make sure the requestId is the one returned from the serverledge node that will execute 21 | func SubmitAsyncWorkflowInvocationRequest(req *Request) { 22 | executionReport, errInvoke := req.W.Invoke(req) 23 | if errInvoke != nil { 24 | log.Println(errInvoke) 25 | PublishAsyncInvocationResponse(req.Id, InvocationResponse{Success: false}) 26 | return 27 | } 28 | reports := make(map[string]*function.ExecutionReport) 29 | req.ExecReport.Reports.Range(func(id ExecutionReportId, report *function.ExecutionReport) bool { 30 | reports[string(id)] = report 31 | return true 32 | }) 33 | PublishAsyncInvocationResponse(req.Id, InvocationResponse{ 34 | Success: true, 35 | Result: req.ExecReport.Result, 36 | Reports: reports, 37 | ResponseTime: req.ExecReport.ResponseTime, 38 | }) 39 | req.ExecReport = executionReport 40 | req.ExecReport.ResponseTime = time.Now().Sub(req.Arrival).Seconds() 41 | } 42 | -------------------------------------------------------------------------------- /internal/workflow/start_task.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/lithammer/shortuuid" 9 | "github.com/serverledge-faas/serverledge/internal/types" 10 | ) 11 | 12 | // StartTask is a Task from which the execution of the Workflow starts. Invokes the first Task 13 | type StartTask struct { 14 | Id TaskId 15 | Type TaskType 16 | Next TaskId 17 | } 18 | 19 | func NewStartTask() *StartTask { 20 | return &StartTask{ 21 | Id: TaskId(shortuuid.New()), 22 | Type: Start, 23 | } 24 | } 25 | 26 | func (s *StartTask) Equals(cmp types.Comparable) bool { 27 | switch cmp.(type) { 28 | case *StartTask: 29 | return s.Next == cmp.(*StartTask).Next 30 | default: 31 | return false 32 | } 33 | } 34 | 35 | func (s *StartTask) AddOutput(workflow *Workflow, taskId TaskId) error { 36 | task, found := workflow.Find(taskId) 37 | if !found { 38 | return fmt.Errorf("task %s not found", taskId) 39 | } 40 | switch task.(type) { 41 | case *StartTask: 42 | return errors.New(fmt.Sprintf("you cannot add an result of type %s to a %s", reflect.TypeOf(task), reflect.TypeOf(s))) 43 | default: 44 | s.Next = taskId 45 | } 46 | return nil 47 | } 48 | 49 | func (s *StartTask) execute(progress *Progress, partialData *PartialData) (*PartialData, *Progress, bool, error) { 50 | 51 | progress.Complete(s.GetId()) 52 | err := progress.AddReadyTask(s.GetNext()[0]) 53 | if err != nil { 54 | return nil, progress, false, err 55 | } 56 | return partialData, progress, true, nil 57 | } 58 | 59 | func (s *StartTask) GetNext() []TaskId { 60 | // we only have one output 61 | return []TaskId{s.Next} 62 | } 63 | 64 | func (s *StartTask) Width() int { 65 | return 1 66 | } 67 | 68 | func (s *StartTask) Name() string { 69 | return "Start " 70 | } 71 | 72 | func (s *StartTask) String() string { 73 | return fmt.Sprintf("[%s]-next->%s", s.Name(), s.Next) 74 | } 75 | 76 | func (s *StartTask) GetId() TaskId { 77 | return s.Id 78 | } 79 | 80 | func (s *StartTask) GetType() TaskType { 81 | return s.Type 82 | } 83 | -------------------------------------------------------------------------------- /internal/workflow/succeed_task.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lithammer/shortuuid" 6 | "github.com/serverledge-faas/serverledge/internal/types" 7 | ) 8 | 9 | type SuccessTask struct { 10 | Id TaskId 11 | Type TaskType 12 | InputPath string 13 | OutputPath string 14 | OutputTo TaskId 15 | } 16 | 17 | func NewSuccessTask() *SuccessTask { 18 | return &SuccessTask{ 19 | Id: TaskId(shortuuid.New()), 20 | Type: Succeed, 21 | } 22 | } 23 | 24 | func (s *SuccessTask) Equals(cmp types.Comparable) bool { 25 | s2, ok := cmp.(*SuccessTask) 26 | if !ok { 27 | return false 28 | } 29 | return s.Id == s2.Id && 30 | s.Type == s2.Type && 31 | s.InputPath == s2.InputPath && 32 | s.OutputPath == s2.OutputPath && 33 | s.OutputTo == s2.OutputTo 34 | } 35 | 36 | func (s *SuccessTask) AddOutput(workflow *Workflow, taskId TaskId) error { 37 | _, ok := workflow.Tasks[taskId].(*EndTask) 38 | if !ok { 39 | return fmt.Errorf("the SuccessTask can only be chained to an end task") 40 | } 41 | s.OutputTo = taskId 42 | return nil 43 | } 44 | 45 | func (s *SuccessTask) GetNext() []TaskId { 46 | return []TaskId{s.OutputTo} 47 | } 48 | 49 | func (s *SuccessTask) Width() int { 50 | return 1 51 | } 52 | 53 | func (s *SuccessTask) Name() string { 54 | return "Success" 55 | } 56 | 57 | func (s *SuccessTask) String() string { 58 | return "[Succeed]" 59 | } 60 | 61 | func (s *SuccessTask) GetId() TaskId { 62 | return s.Id 63 | } 64 | 65 | func (s *SuccessTask) GetType() TaskType { 66 | return s.Type 67 | } 68 | -------------------------------------------------------------------------------- /internal/workflow/task.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/serverledge-faas/serverledge/internal/types" 7 | ) 8 | 9 | type TaskId string 10 | 11 | // Task is an interface for a single node in the Workflow 12 | // all implementors must be pointers to a struct 13 | type Task interface { 14 | types.Comparable 15 | Display 16 | AddOutput(workflow *Workflow, taskId TaskId) error 17 | GetType() TaskType 18 | GetNext() []TaskId 19 | Width() int // TODO: is this really needed? 20 | } 21 | 22 | type Display interface { 23 | fmt.Stringer 24 | GetId() TaskId 25 | Name() string 26 | } 27 | 28 | func Equals[D Task](d1 D, d2 D) bool { 29 | return d1.Equals(d2) 30 | } 31 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | 6 | scrape_configs: 7 | # The job name is added as a label `job=` to any timeseries scraped from this config. 8 | - job_name: "serverledge" 9 | 10 | # metrics_path defaults to '/metrics' 11 | # scheme defaults to 'http'. 12 | 13 | static_configs: 14 | - targets: ["172.17.0.1:2112"] 15 | 16 | 17 | -------------------------------------------------------------------------------- /scripts/compositions/create-composition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli workflow-create -f sequence -j sequence 6 | -------------------------------------------------------------------------------- /scripts/compositions/delete-composition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli delete-workflow -c sequence 6 | -------------------------------------------------------------------------------- /scripts/compositions/get-composition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli list-workflows 6 | -------------------------------------------------------------------------------- /scripts/compositions/invoke-async-composition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli invoke-workflow -f sequence -p "input:1" --async 6 | -------------------------------------------------------------------------------- /scripts/compositions/invoke-composition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli invoke-workflow -f sequence -p "input:1" 6 | -------------------------------------------------------------------------------- /scripts/compositions/poll-composition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli poll --request "$1" -------------------------------------------------------------------------------- /scripts/defrag-etcd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | REVISION=$(ETCDCTL_API=3 etcdctl endpoint status --write-out="json" | egrep -o '"revision":[0-9]*' | egrep -o '[0-9].*') 4 | ETCDCTL_API=3 etcdctl compact ${REVISION} 5 | ETCDCTL_API=3 etcdctl defrag 6 | etcdctl alarm disarm -------------------------------------------------------------------------------- /scripts/experiments/experiment1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | THIS_DIR=$(dirname "$0") 3 | locust -f "${THIS_DIR}"/../../examples/experiments/experiment1/locust1.py -H http://192.168.1.14:1323 -------------------------------------------------------------------------------- /scripts/experiments/experiment2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | THIS_DIR=$(dirname "$0") 3 | locust -f "${THIS_DIR}"/../../examples/experiments/experiment2/locust2.py -H http://192.168.1.14:1323 -------------------------------------------------------------------------------- /scripts/functions/create-function.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli create -f inc --memory 40 --src "$THIS_DIR"/../../examples/inc.py --runtime python310 --handler "inc.handler" -------------------------------------------------------------------------------- /scripts/functions/delete-function.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli delete -f inc -------------------------------------------------------------------------------- /scripts/functions/get-functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli list -------------------------------------------------------------------------------- /scripts/functions/invoke-async-function.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli invoke -f inc -p "input:1" --async -------------------------------------------------------------------------------- /scripts/functions/invoke-function.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | 5 | "$THIS_DIR"/../../bin/serverledge-cli invoke -f inc -p "input:1" -------------------------------------------------------------------------------- /scripts/functions/poll-invocation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | THIS_DIR=$(dirname "$0") 4 | echo $1 5 | "$THIS_DIR"/../../bin/serverledge-cli poll --request $1 -------------------------------------------------------------------------------- /scripts/list-etcd-keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker exec Etcd-server etcdctl get "" --prefix -------------------------------------------------------------------------------- /scripts/remove-etcd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker rm -f Etcd-server 3 | -------------------------------------------------------------------------------- /scripts/simple-benchmark.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | 4 | bin/serverledge-cli delete -f sieve 5 | set -e 6 | 7 | bin/serverledge-cli create -f sieve --memory 128 --src examples/sieve.js --runtime nodejs17ng --handler "sieve.js" && sleep 1 8 | 9 | echo '{"Params":{},"QoSClass":0}' > /tmp/json 10 | ab -l -p /tmp/json -T application/json -c 5 -n 25000 http://127.0.0.1:1323/invoke/sieve | tee ab_output.txt 11 | -------------------------------------------------------------------------------- /scripts/start-etcd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker run -d --rm --name Etcd-server \ 3 | --publish 2379:2379 \ 4 | --publish 2380:2380 \ 5 | --cpus="1" \ 6 | --env ALLOW_NONE_AUTHENTICATION=yes \ 7 | --env ETCD_ADVERTISE_CLIENT_URLS=http://localhost:2379 \ 8 | bitnami/etcd:3.5.14-debian-12-r1 9 | -------------------------------------------------------------------------------- /scripts/start-prometheus.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker run \ 3 | --name prometheusLocal \ 4 | -d --rm \ 5 | -p 9090:9090 \ 6 | -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \ 7 | prom/prometheus:v2.37.1 \ 8 | --config.file=/etc/prometheus/prometheus.yml 9 | -------------------------------------------------------------------------------- /utils/convert.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | func ConvertToSlice(v interface{}) ([]interface{}, error) { 9 | var out []interface{} 10 | rv := reflect.ValueOf(v) 11 | if rv.Kind() == reflect.Slice { 12 | for i := 0; i < rv.Len(); i++ { 13 | out = append(out, rv.Index(i).Interface()) 14 | } 15 | } else { 16 | return nil, fmt.Errorf("cannot convert interface to interface slice") 17 | } 18 | return out, nil 19 | } 20 | 21 | func ConvertToSpecificSlice[T any](slice []interface{}) ([]T, error) { 22 | out := make([]T, 0, len(slice)) 23 | for _, value := range slice { 24 | castedValue, ok := value.(T) 25 | if !ok { 26 | return nil, fmt.Errorf("failed to convert to generic type") 27 | } 28 | out = append(out, castedValue) 29 | } 30 | 31 | return out, nil 32 | } 33 | 34 | func ConvertInterfaceToSpecificSlice[T any](val interface{}) ([]T, error) { 35 | slice, err := ConvertToSlice(val) 36 | if err != nil { 37 | return nil, err 38 | } 39 | specificSlice, err := ConvertToSpecificSlice[T](slice) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return specificSlice, nil 44 | } 45 | -------------------------------------------------------------------------------- /utils/errors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // ReturnNonNilErr returns the first non-nil. If all errors are nil, returns nil. 4 | func ReturnNonNilErr(errs ...error) error { 5 | for _, e := range errs { 6 | if e != nil { 7 | return e 8 | } 9 | } 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /utils/etcd.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/serverledge-faas/serverledge/internal/config" 9 | clientv3 "go.etcd.io/etcd/client/v3" 10 | ) 11 | 12 | var etcdClient *clientv3.Client = nil 13 | var clientMutex sync.Mutex 14 | 15 | func GetEtcdClient() (*clientv3.Client, error) { 16 | clientMutex.Lock() 17 | defer clientMutex.Unlock() 18 | 19 | // reuse client 20 | if etcdClient != nil { 21 | return etcdClient, nil 22 | } 23 | 24 | etcdHost := config.GetString(config.ETCD_ADDRESS, "localhost:2379") 25 | cli, err := clientv3.New(clientv3.Config{ 26 | Endpoints: []string{etcdHost}, 27 | DialTimeout: 1 * time.Second, 28 | }) 29 | if err != nil { 30 | return nil, fmt.Errorf("Could not connect to etcd: %v", err) 31 | } 32 | 33 | etcdClient = cli 34 | return cli, nil 35 | } 36 | -------------------------------------------------------------------------------- /utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | func PostJson(url string, body []byte) (*http.Response, error) { 14 | resp, err := http.Post(url, "application/json", bytes.NewBuffer(body)) 15 | if err != nil { 16 | return nil, err 17 | } 18 | if resp.StatusCode != http.StatusOK { 19 | return resp, fmt.Errorf("Server response: %v", resp.Status) 20 | } 21 | return resp, nil 22 | } 23 | 24 | func PostJsonIgnore409(url string, body []byte) (*http.Response, error) { 25 | resp, err := http.Post(url, "application/json", bytes.NewBuffer(body)) 26 | if err != nil { 27 | return nil, err 28 | } 29 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusConflict { 30 | return resp, fmt.Errorf("Server response: %v", resp.Status) 31 | } 32 | return resp, nil 33 | } 34 | 35 | func PrintJsonResponse(resp io.ReadCloser) { 36 | defer func(resp io.ReadCloser) { 37 | err := resp.Close() 38 | if err != nil { 39 | fmt.Printf("Error while closing JSON reader: %s\n", err) 40 | } 41 | }(resp) 42 | body, _ := io.ReadAll(resp) 43 | 44 | // print indented JSON 45 | var out bytes.Buffer 46 | err := json.Indent(&out, body, "", "\t") 47 | if err != nil { 48 | fmt.Printf("Error while indenting JSON: %s\n", err) 49 | return 50 | } 51 | _, err = out.WriteTo(os.Stdout) 52 | if err != nil { 53 | fmt.Printf("Error while writing indented JSON to stdout: %s\n", err) 54 | return 55 | } 56 | } 57 | 58 | func PrintErrorResponse(resp io.ReadCloser) { 59 | defer resp.Close() 60 | body, err := io.ReadAll(resp) 61 | if err != nil { 62 | fmt.Println("Error reading response:", err) 63 | return 64 | } 65 | 66 | // Convert the []byte to a string 67 | bodyStr := string(body) 68 | if bodyStr == "" { 69 | return 70 | } 71 | // Replace "\n" with actual newline characters 72 | formatted := strings.ReplaceAll(bodyStr, "\\n", "\n") 73 | formatted2 := strings.ReplaceAll(formatted, "\"", "") 74 | formatted3 := strings.ReplaceAll(formatted2, "{", "\n") 75 | formatted4 := strings.ReplaceAll(formatted3, "}", "") 76 | 77 | fmt.Print(formatted4) 78 | } 79 | 80 | func GetJsonResponse(resp io.ReadCloser) string { 81 | defer resp.Close() 82 | body, _ := io.ReadAll(resp) 83 | 84 | var out bytes.Buffer 85 | json.Indent(&out, body, "", "\t") 86 | return out.String() 87 | } 88 | -------------------------------------------------------------------------------- /utils/networking.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // GetOutboundIp retrieves the host ip address by Dialing with Google's DNS (cross-platform) 9 | func GetOutboundIp() (net.IP, error) { 10 | conn, err := net.Dial("udp", "8.8.8.8:80") 11 | if err != nil { 12 | return net.IP{}, fmt.Errorf("could not get UDP address - check internet connection: %v", err) 13 | } 14 | 15 | defer func() { 16 | _ = conn.Close() 17 | }() 18 | 19 | localAddr := conn.LocalAddr().(*net.UDPAddr) 20 | return localAddr.IP, nil 21 | } 22 | -------------------------------------------------------------------------------- /utils/tar.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "archive/tar" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | func Tar(src string, of *os.File) error { 13 | 14 | if _, err := os.Stat(src); err != nil { 15 | return fmt.Errorf("Unable to tar files - %v", err.Error()) 16 | } 17 | 18 | //of, err := os.Create(outFile) 19 | //if err != nil { 20 | // return fmt.Errorf("Could not create tarball file '%s', got error '%s'", outFile, err.Error()) 21 | //} 22 | //defer of.Close() 23 | 24 | tw := tar.NewWriter(of) 25 | defer func(tw *tar.Writer) { 26 | err := tw.Close() 27 | if err != nil { 28 | fmt.Printf("Error while closing tar writer: %s\n", err) 29 | } 30 | }(tw) 31 | 32 | return filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { 33 | 34 | if err != nil { 35 | fmt.Printf("Generic error for %v: %v\n", fi, err) 36 | return err 37 | } 38 | 39 | // skip non-regular files 40 | if !fi.Mode().IsRegular() { 41 | return nil 42 | } 43 | 44 | // create a new dir/file header 45 | header, err := tar.FileInfoHeader(fi, fi.Name()) 46 | if err != nil { 47 | fmt.Printf("Cannot create file header for %v\n", fi) 48 | return err 49 | } 50 | 51 | // update the name to correctly reflect the desired destination when untaring 52 | var strippedSrc string 53 | if filepath.Dir(src) == "." && !strings.HasPrefix(src, ".") { 54 | strippedSrc = src // nothing to do 55 | } else { 56 | strippedSrc = strings.Replace(file, filepath.Dir(src), "", -1) 57 | } 58 | 59 | header.Name = strings.TrimPrefix(strippedSrc, string(filepath.Separator)) 60 | 61 | // write the header 62 | if err := tw.WriteHeader(header); err != nil { 63 | fmt.Printf("Cannot write file header for %v\n", fi) 64 | return err 65 | } 66 | 67 | // open files for taring 68 | f, err := os.Open(file) 69 | if err != nil { 70 | fmt.Printf("Cannot open file %v\n", fi) 71 | return err 72 | } 73 | 74 | // copy file data into tar writer 75 | if _, err := io.Copy(tw, f); err != nil { 76 | fmt.Printf("Cannot write file %v\n", fi) 77 | return err 78 | } 79 | 80 | err = f.Close() 81 | if err != nil { 82 | fmt.Printf("Error while closing file '%s': %s\n", f.Name(), err) 83 | return err 84 | } 85 | 86 | return nil 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /utils/testing.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "golang.org/x/exp/maps" 5 | "golang.org/x/exp/slices" 6 | "testing" 7 | ) 8 | 9 | func AssertEquals[T comparable](t *testing.T, expected T, result T) { 10 | if expected != result { 11 | t.Logf("%s is failed. Got '%v', expected '%v'", t.Name(), result, expected) 12 | t.FailNow() 13 | } 14 | } 15 | 16 | func AssertEqualsMsg[T comparable](t *testing.T, expected T, result T, msg string) { 17 | if expected != result { 18 | t.Logf("%s is failed; %s - Got '%v', expected '%v'", t.Name(), msg, result, expected) 19 | t.FailNow() 20 | } 21 | } 22 | 23 | func AssertSliceEquals[T comparable](t *testing.T, expected []T, result []T) { 24 | if equal := slices.Equal(expected, result); !equal { 25 | t.Logf("%s is failed Got '%v', expected '%v'", t.Name(), result, expected) 26 | t.FailNow() 27 | } 28 | } 29 | 30 | func AssertSliceEqualsMsg[T comparable](t *testing.T, expected []T, result []T, msg string) { 31 | if equal := slices.Equal(expected, result); !equal { 32 | t.Logf("%s is failed; %s - Got '%v', expected '%v'", t.Name(), msg, result, expected) 33 | t.FailNow() 34 | } 35 | } 36 | 37 | func AssertMapEquals[K comparable, V comparable](t *testing.T, expectedMap map[K]V, resultMap map[K]interface{}) { 38 | typedMap := make(map[K]V) 39 | for k, v := range resultMap { 40 | typedMap[k] = v.(V) 41 | } 42 | if equal := maps.Equal(expectedMap, typedMap); !equal { 43 | t.Logf("%s is failed. Got '%v', expected '%v'", t.Name(), resultMap, expectedMap) 44 | t.FailNow() 45 | } 46 | } 47 | 48 | func AssertNil(t *testing.T, result interface{}) { 49 | if nil != result { 50 | t.Logf("%s is failed. Got '%v', expected nil", t.Name(), result) 51 | t.FailNow() 52 | } 53 | } 54 | 55 | func AssertNilMsg(t *testing.T, result interface{}, msg string) { 56 | if nil != result { 57 | t.Logf("%s is failed; %s - Got '%v', expected nil", t.Name(), result, msg) 58 | t.FailNow() 59 | } 60 | } 61 | 62 | func AssertNonNil(t *testing.T, result interface{}) { 63 | if nil == result { 64 | t.Logf("%s is failed. Got '%v', expected non-nil", t.Name(), result) 65 | t.FailNow() 66 | } 67 | } 68 | 69 | func AssertNonNilMsg(t *testing.T, result interface{}, msg string) { 70 | if nil == result { 71 | t.Logf("%s is failed; %s - Got '%v', expected non-nil", t.Name(), result, msg) 72 | t.FailNow() 73 | } 74 | } 75 | 76 | // AssertNotEmptySlice asserts that a slice is not empty. Notice: here we use generics. Only works for go 1.19+ 77 | func AssertNotEmptySlice[A any](t *testing.T, slice []*A) { 78 | if slice == nil { 79 | t.Logf("%s is failed. The slice is nil,", t.Name()) 80 | t.FailNow() 81 | } 82 | if len(slice) == 0 { 83 | t.Logf("%s is failed. The slice is empty,", t.Name()) 84 | t.FailNow() 85 | } 86 | } 87 | 88 | func AssertEmptySlice[T any](t *testing.T, slice []T) { 89 | if slice == nil { 90 | t.Logf("%s is failed. The slice is nil,", t.Name()) 91 | t.FailNow() 92 | } 93 | if len(slice) != 0 { 94 | t.Logf("%s is failed. The slice is NOT empty,", t.Name()) 95 | t.FailNow() 96 | } 97 | } 98 | 99 | func AssertTrue(t *testing.T, isTrue bool) { 100 | if !isTrue { 101 | t.Logf("%s is failed. Got false", t.Name()) 102 | t.FailNow() 103 | } 104 | } 105 | 106 | func AssertTrueMsg(t *testing.T, isTrue bool, msg string) { 107 | if !isTrue { 108 | t.Logf("%s is false - %s", t.Name(), msg) 109 | t.FailNow() 110 | } 111 | } 112 | 113 | func AssertFalse(t *testing.T, isTrue bool) { 114 | if isTrue { 115 | t.Logf("%s is failed. Got true", t.Name()) 116 | t.FailNow() 117 | } 118 | } 119 | 120 | func AssertFalseMsg(t *testing.T, isTrue bool, msg string) { 121 | if isTrue { 122 | t.Logf("%s is true - %s", t.Name(), msg) 123 | t.FailNow() 124 | } 125 | } 126 | --------------------------------------------------------------------------------