WasmHost
The WasmHost
is a filter of Easegress which can be orchestrated into a pipeline. But while the behavior of all other filters are defined by filter developers and can only be fine-tuned by configuration, this filter implements a host environment for user-developed WebAssembly code, which enables users to control the filter behavior completely.
Why Use WasmHost
- Zero Down Time: filter behavior can be modified by a hot update.
- Fast Develop, Fast Deploy: we believe everyone can be a software developer, and you know your requirement better, no need to wait for MegaEase.
- Every Thing Under Control: the filter behavior is right under your fingers.
- Develop with Your Favour Language: choose one from
AssemblyScript
,Go
,C/C++
,Rust
, and so on at your wish (Note: a language-specific SDK is required, we are working on this).
Examples
Note: The WasmHost
filter is disabled by default, to enable it, you need to build Easegress with the below command:
$ make build_server GOTAGS=wasmhost
We will use AssemblyScript as the language of the examples, please ensure a recent version of Git, Golang, Node.js and its package manager npm are installed before continue. Basic knowledge about writing and working with TypeScript modules, which is very similar to AssemblyScript, is a plus.
Basic: Noop
The AssemblyScript code of this example is just a noop. But this example includes all the required steps to write, build and deploy WebAssembly code, and it shows the basic structure of the code. All latter examples are based on this one.
Clone git repository
easegress-assemblyscript-sdk
to someware on disk$ git clone https://github.com/megaease/easegress-assemblyscript-sdk.git
Switch to a new directory and initialize a new node module:
npm init
Install the AssemblyScript compiler using npm, assume that the compiler is not required in production and make it a development dependency:
npm install --save-dev assemblyscript
Once installed, the compiler provides a handy scaffolding utility to quickly set up a new AssemblyScript project, for example in the directory of the just initialized node module:
npx asinit .
Add
--use abort=
to theasc
inpackage.json
, for example:"asbuild:untouched": "asc assembly/index.ts --target debug --use abort=", "asbuild:optimized": "asc assembly/index.ts --target release --use abort=",
Replace the content of
assembly/index.ts
with the code below, note to replace{EASEGRESS_SDK_PATH}
with the path in step 1:// this line exports everything required by Easegress, export * from '{EASEGRESS_SDK_PATH}/easegress/proxy' // import everything you need from the SDK, import { Program, registerProgramFactory } from '{EASEGRESS_SDK_PATH}/easegress' // define the program, 'Noop' is the name class Noop extends Program { // constructor is the initializer of the program, will be called once at the startup constructor(params: Map<string, string>) { super(params) } // run will be called for every request run(): i32 { return 0 } } // register a factory method of the program, the only thing you // may want to change is the program name, here is 'Noop' registerProgramFactory((params: Map<string, string>) => { return new Noop(params) })
Build with the below command, if everything is right,
untouched.wasm
(the debug version) andoptimized.wasm
(the release version) will be generated at thebuild
folder.$ npm run asbuild
Create an HTTPServer in Easegress to listen on port 10080 to handle the HTTP traffic:
$ echo ' kind: HTTPServer name: server-demo port: 10080 keepAlive: true https: false rules: - paths: - pathPrefix: /pipeline backend: wasm-pipeline' | egctl create -f -
Create pipeline
wasm-pipeline
which includes aWasmHost
filter:$ echo ' name: wasm-pipeline kind: Pipeline flow: - filter: wasm - filter: proxy filters: - name: wasm kind: WasmHost maxConcurrency: 2 code: /home/megaease/example/build/optimized.wasm timeout: 100ms - name: proxy kind: Proxy pools: - servers: - url: http://127.0.0.1:9095 loadBalance: policy: roundRobin' | egctl create -f -
Note to replace
/home/megaease/example/build/optimized.wasm
with the path of the file generated in step 7.Set up a backend service at
http://127.0.0.1:9095
with themirror
server from the Easegress repository. Using another HTTP server is fine, but themirror
server prints requests it received to the console, which makes it much easier to verify the results:$ git clone https://github.com/megaease/easegress.git $ cd easegress $ go run example/backend-service/mirror/mirror.go
Execute below command in a new console to verify what we have done.
$ curl http://127.0.0.1:10080/pipeline -d 'Hello, Easegress' Your Request ============== Method: POST URL : /pipeline Header: User-Agent: [curl/7.68.0] Accept: [*/*] Content-Type: [application/x-www-form-urlencoded] Accept-Encoding: [gzip] Body : Hello, Easegress
Add a New Header
export * from '{EASEGRESS_SDK_PATH}/easegress/proxy'
import { Program, request, registerProgramFactory } from '{EASEGRESS_SDK_PATH}/easegress'
class AddHeader extends Program {
run(): i32 {
request.addHeader('Wasm-Added', 'I was added by WebAssembly')
return 0
}
}
registerProgramFactory((params: Map<string, string>) => {
return new AddHeader(params)
})
Build and verify with:
$ npm run asbuild
$ egctl wasm apply-data --reload-code
$ curl http://127.0.0.1:10080/pipeline -d 'Hello, Easegress'
Your Request
==============
Method: POST
URL : /pipeline
Header:
Accept-Encoding: [gzip]
User-Agent: [curl/7.68.0]
Accept: [*/*]
Content-Type: [application/x-www-form-urlencoded]
Wasm-Added: [I was added by WebAssembly]
Body : Hello, Easegress
Add a New Header According to Configuration
export * from '{EASEGRESS_SDK_PATH}/easegress/proxy'
import { Program, request, registerProgramFactory } from '{EASEGRESS_SDK_PATH}/easegress'
class AddHeader extends Program {
headerName: string
headerValue: string
constructor(params: Map<string, string>) {
this.headerName = params.get("headerName")
this.headerValue = params.get("headerValue")
super(params)
}
run(): i32 {
request.addHeader(this.headerName, this.headerValue)
return 0
}
}
registerProgramFactory((params: Map<string, string>) => {
return new AddHeader(params)
})
And we also need to modify the filter configuration to add headerName
and headerValue
as parameters:
filters:
- name: wasm
kind: WasmHost
parameters: # +
headerName: "Wasm-Added-2" # +
headerValue: "I was added by WebAssembly too" # +
Build and verify with:
$ npm run asbuild
$ egctl wasm apply-data --reload-code
$ curl http://127.0.0.1:10080/pipeline -d 'Hello, Easegress'
Your Request
==============
Method: POST
URL : /pipeline
Header:
Accept-Encoding: [gzip]
User-Agent: [curl/7.68.0]
Accept: [*/*]
Content-Type: [application/x-www-form-urlencoded]
Wasm-Added-2: [I was added by WebAssembly too]
Add a Cookie
export * from '{EASEGRESS_SDK_PATH}/easegress/proxy'
import { Program, cookie, response, registerProgramFactory } from '{EASEGRESS_SDK_PATH}/easegress'
class AddCookie extends Program {
run(): i32 {
let c = new cookie.Cookie()
c.name = "wasm"
c.value = "2021-07-29"
c.httpOnly = true
request.addCookie(c)
return 0
}
}
registerProgramFactory((params: Map<string, string>) => {
return new AddCookie(params)
})
Build and verify with:
$ npm run asbuild
$ egctl wasm apply-data --reload-code
$ curl http://127.0.0.1:10080/pipeline -d 'Hello, Easegress'
Your Request
==============
Method: POST
URL : /pipeline
Header:
User-Agent: [curl/7.68.0]
Accept: [*/*]
Content-Type: [application/x-www-form-urlencoded]
Cookie: [wasm=2021-07-29; HttpOnly=]
Accept-Encoding: [gzip]
Body : Hello, Easegress
Mock Response
export * from '{EASEGRESS_SDK_PATH}/easegress/proxy'
import { Program, response, registerProgramFactory } from '{EASEGRESS_SDK_PATH}/easegress'
class MockResponse extends Program {
run(): i32 {
response.setStatusCode(200)
response.setBody(String.UTF8.encode("I have a new body now"))
return 0
}
}
registerProgramFactory((params: Map<string, string>) => {
return new MockResponse(params)
})
Because we are mocking a response, we need to remove the proxy
from pipeline:
$ echo '
name: wasm-pipeline
kind: Pipeline
flow:
- filter: wasm
filters:
- name: wasm
kind: WasmHost
maxConcurrency: 2
code: /home/megaease/example/build/optimized.wasm
timeout: 100ms' | egctl apply -f -
Build and verify with:
$ npm run asbuild
$ egctl wasm reload-code
$ curl http://127.0.0.1:10080/pipeline -d 'Hello, Easegress'
I have a new body now
Access Shared Data
When the maxConcurrency
field of a WasmHost filter is larger than 1
, or when Easegress is deployed as a cluster, a single WasmHost filter could have more than one Wasm Virtual Machine
s. Because safety is the design principle of WebAssembly, these VMs are isolated and can not share data with each other.
But sometimes, sharing data is useful, Easegress provides APIs for this:
export * from '{EASEGRESS_SDK_PATH}/easegress/proxy'
import { Program, response, cluster, registerProgramFactory } from '{EASEGRESS_SDK_PATH}/easegress'
class SharedData extends Program {
run(): i32 {
let counter = cluster.AddInteger("counter", 1)
response.setStatusCode(200)
response.setBody(String.UTF8.encode("number of requests is: ", counter.toString()))
return 0
}
}
registerProgramFactory((params: Map<string, string>) => {
return new SharedData(params)
})
Suppose we are using the same pipeline configuration as in Mock Response, we can build and verify this example with:
$ npm run asbuild
$ egctl wasm reload-code
$ curl http://127.0.0.1:10080/pipeline
number of requests is 1
$ curl http://127.0.0.1:10080/pipeline
number of requests is 2
$ curl http://127.0.0.1:10080/pipeline
number of requests is 3
We can view the shared data with:
$ egctl wasm list-data wasm-pipeline wasm
counter: "3"
where wasm-pipeline
is the pipeline name and wasm
is the filter name.
The shared data can be modified with:
$ echo 'counter: 100' | egctl wasm apply-data wasm-pipeline wasm
$ curl http://127.0.0.1:10080/pipeline
number of requests is 101
And can be deleted with:
$ egctl wasm delete-data wasm-pipeline wasm
Return a Result Other Than 0
export * from '{EASEGRESS_SDK_PATH}/easegress/proxy'
import { Program, request, registerProgramFactory } from '{EASEGRESS_SDK_PATH}/easegress'
class NonZeroResult extends Program {
run(): i32 {
let token = request.getHeader("Authorization")
if(token == "") {
return 1
}
return 0
}
}
registerProgramFactory((params: Map<string, string>) => {
return new NonZeroResult(params)
})
Return value 1
will be converted to wasmResult1
by the WasmHost
filter, we can use this result in the pipeline configuration:
flow:
- filter: wasm
jumpIf: { wasmResult1: END } # +
- filter: proxy
Build and verify with:
$ npm run asbuild
$ egctl wasm reload-code
$ curl http://127.0.0.1:10080/pipeline -d 'Hello, Easegress'
$ curl http://127.0.0.1:10080/pipeline -d 'Hello, Easegress' -HAuthorization:abc
Your Request
==============
Method: POST
URL : /pipeline
Header:
Accept-Encoding: [gzip]
User-Agent: [curl/7.68.0]
Accept: [*/*]
Authorization: [abc]
Content-Type: [application/x-www-form-urlencoded]
Body : Hello, Easegress