Repository: megaease/easegress
Pull request #164 (2021-08-04 02:36:44)

[bug] fix interface nil convert panic
It will cause an unrecovered panic if we are looking for a not exist pipeline by trafficcontroller.

``` bash
panic: interface conversion: interface {} is nil, not *supervisor.ObjectEntity

goroutine 1547 [running]:
github.com/megaease/easegress/pkg/object/trafficcontroller.(*TrafficController).GetHTTPPipeline(0xc01719c5d0, 0x34a9e04, 0x7, 0xc00dbb0430, 0xc, 0x0, 0x0)
        github.com/megaease/easegress/pkg/object/trafficcontroller/trafficcontroller.go:550 +0x1b9
github.com/megaease/easegress/pkg/object/rawconfigtrafficcontroller.(*RawConfigTrafficController).GetHTTPPipeline(0xc017ed6980, 0xc00dbb0430, 0xc, 0xc017eb1740, 0xc000052836, 0x38f4428)
        github.com/megaease/easegress/pkg/object/rawconfigtrafficcontroller/rawconfigtrafficcontroller.go:95 +0x5c
github.com/megaease/easegress/pkg/filter/apiaggregator.(*APIAggregator).handle.func1(0xc0177c79b0, 0xc00050b2d8, 0xc011e52570, 0x1, 0x1, 0x0, 0xc00dbb0430, 0xc, 0xc017324f00)
        github.com/megaease/easegress/pkg/filter/apiaggregator/apiaggregator.go:205 +0x92
created by github.com/megaease/easegress/pkg/filter/apiaggregator.(*APIAggregator).handle
        github.com/megaease/easegress/pkg/filter/apiaggregator/apiaggregator.go:203 +0x18d
cluster-join-urls [http://127.0.0.1:12380 http://127.0.0.1:22380 http://127.0.0.1:32380] changed to empty because it tries to join itself
```



Patch:
diff --git a/README.md b/README.md
index ce8889bc..7b4dc39d 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
 - [Easegress](#easegress)
   - [What is Easegress](#what-is-easegress)
   - [Features](#features)
+  - [User Cases](#user-cases)
   - [Getting Started](#getting-started)
     - [Setting up Easegress](#setting-up-easegress)
     - [Create an HTTPServer and Pipeline](#create-an-httpserver-and-pipeline)
@@ -15,6 +16,7 @@
     - [More Filters](#more-filters)
   - [Documentation](#documentation)
   - [Roadmap](#roadmap)
+  - [Community](#community)
   - [License](#license)
 
 ## What is Easegress
@@ -66,7 +68,7 @@ The architecture of Easegress:
 		- **Mesh Master:** is the control plane to manage the lifecycle of mesh services.
 		- **Mesh Sidecar:** is the data plane as the endpoint to do traffic interception and routing.
 		- **Mesh Ingress Controller:** is the mesh-specific ingress controller to route external traffic to mesh services.
-		  > Notes: This feature is leveraged by [EaseMesh](https://github.com/megaease/easemesh) 
+		  > Notes: This feature is leveraged by [EaseMesh](https://github.com/megaease/easemesh)
 	- **Third-Part Integration**
 		- **FaaS** integrates with the serverless platform Knative.
 		- **Service Discovery** integrates with Eureka, Consul, Etcd, and Zookeeper.
@@ -94,6 +96,26 @@ The architecture of Easegress:
 			- **Status Codes:** HTTP status codes.
 			- **TopN:** sorted by aggregated APIs(only in server dimension).
 
+
+## User Cases
+
+The following examples show how to use Easegress for different scenarios.
+
+- [API Aggregator](./doc/cookbook/api_aggregator.md) - Aggregating many APIs into a single API.
+- [FaaS](./doc/cookbook/faas.md) - Supporting Knative FaaS integration
+- [Flash Sale](./doc/cookbook/flash_sale.md) - How to do high concurrent promotion sales with Easegress
+- [LoadBalancer](./doc/cookbook/load_balancer.md) - A number of strategy of load balancing 
+- [Distributed Tracing](./doc/cookbook/distributed_tracing.md) - How to do APM tracing  - Zipkin.
+- [Kubernetes Ingress Controller](./doc/cookbook/k8s_ingress_controller.md) - How to integrated with Kubernetes as ingress controller
+- [Performance](./doc/cookbook/performance.md) - Performance optimization - compression, caching etc.
+- [Resilience and Fault Tolerance](./doc/cookbook/resilience.md) - Circuit Breaker, Rate Lmiter, Retryer, Time limiter, etc. (Porting from [Java resilience4j](https://github.com/resilience4j/resilience4j))
+- [Security](./doc/cookbook/security.md) - How to do authenication by Header, JWT, HMAC, OAuth2, etc.
+- [Service Proxy](./doc/cookbook/service_proxy.md) - Supporting the Microservice  registries - Zookeeper, Eureka, Consul, Nacos, etc.
+- [WebAssembly](./doc/cookbook/wasm.md) - Using AssemblyScript to extend the Easegress
+- [Workflow](./doc/cookbook/workflow.md) - An Example to make a workflow for a number of APIs.
+
+For full list, see [Cookbook](./doc/cookbook/README.md).
+
 ## Getting Started
 
 The basic common usage of Easegress is to quickly set up proxy for the backend servers. We split it into multiple simple steps to illustrate the essential concepts and operations.
@@ -290,14 +312,21 @@ Body  : {"message": "Hello, Easegress"}
 
 We can also see Easegress send one more header `X-Adapt-Key: goodplan` to the mirror service.
 
+
 ## Documentation
 
 See [reference](./doc/reference.md) and [developer guide](./doc/developer-guide.md) for more information.
 
-## Roadmap 
+## Roadmap
 
 See [Easegress Roadmap](./doc/Roadmap.md) for details.
 
+## Community
+
+- [Slack Channel](https://openmegaease.slack.com/archives/C028Z6SUCU9) for requirement, issue and development.
+- [MegaEase on Twitter](https://twitter.com/megaease)
+
+
 ## License
 
 Easegress is under the Apache 2.0 license. See the [LICENSE](./LICENSE) file for details.
diff --git a/doc/cookbook/README.md b/doc/cookbook/README.md
new file mode 100644
index 00000000..382a8afe
--- /dev/null
+++ b/doc/cookbook/README.md
@@ -0,0 +1,16 @@
+# Cookbook for Common User Cases
+
+The following examples show how to use Easegress for different scenarios.
+
+- [API Aggregator](./api_aggregator.md) - Aggregating many APIs into a single API.
+- [FaaS](./faas.md) - Supporting Knative FaaS integration
+- [Flash Sale](./flash_sale.md) - How to do high concurrent promotion sales with Easegress
+- [LoadBalancer](./load_balancer.md) - A number of strategy of load balancing 
+- [Distributed Tracing](./distributed_tracing.md) - How to do APM tracing  - Zipkin.
+- [Kubernetes Ingress Controller](./k8s_ingress_controller.md) - How to integrated with Kubernetes as ingress controller
+- [Performance](./performance.md) - Performance optimization - compression, caching etc.
+- [Resilience and Fault Tolerance](./resilience.md) - Circuit Breaker, Rate Lmiter, Retryer, Time limiter, etc. (Porting from [Java resilience4j](https://github.com/resilience4j/resilience4j))
+- [Security](./security.md) - How to do authenication by Header, JWT, HMAC, OAuth2, etc.
+- [Service Proxy](./service_proxy.md) - Supporting the Microservice  registries - Zookeeper, Eureka, Consul, Nacos, etc.
+- [WebAssembly](./wasm.md) - Using AssemblyScript to extend the Easegress
+- [Workflow](./workflow.md) - An Example to make a workflow for a number of APIs.
diff --git a/doc/cookbook/api_aggregator.md b/doc/cookbook/api_aggregator.md
new file mode 100644
index 00000000..d6b8b467
--- /dev/null
+++ b/doc/cookbook/api_aggregator.md
@@ -0,0 +1,159 @@
+# API Aggregator
+
+- [API Aggregator](#api-aggregator)
+  - [Background](#background)
+  - [API aggregator](#api-aggregator-1)
+  - [Example](#example)
+    - [Scenario 1: Run an aggregator in pipeline](#scenario-1-run-an-aggregator-in-pipeline)
+    - [Scenario 2: Merge response body](#scenario-2-merge-response-body)
+    - [Scenario 3: Allow partial succeed](#scenario-3-allow-partial-succeed)
+  - [References](#references)
+
+## Background
+
+* API aggregation is a pattern to aggregate multiple individual requests into a single request. This pattern is useful when a client must make multiple calls to different backend systems to operate.[1]
+* Easegress provides a filter called `APIAggregator` in the pipeline for this powerful feature.
+
+## API aggregator
+
+* Reusing the existing pipelines for an API request.
+* Easy to integrate with other filters such as RateLimiter.
+
+## Example
+
+### Scenario 1: Run an aggregator in pipeline
+
+1. We have three pipelines in the default namespace, called  `pipeline-demo`,  `pipeline-demo1`, and  `pipeline-demo2`. We want to call these three pipelines and combine their response with one request.
+`pipeline-demo` will return `{"mega":"ease"}` in HTTP response body.
+`pipeline-demo1` will return `{"hello":"world"}`.
+`pipeline-demo2` will return `{"hello":"new world"}`.
+
+``` bash
+echo '
+name: pipeline-api
+kind: HTTPPipeline
+flow:
+  - filter: agg
+filters:
+  - name: agg
+    kind: APIAggregator
+    pipelines:
+      - name: pipeline-demo
+      - name: pipeline-demo1
+      - name: pipeline-demo2 '  | egctl object create
+```
+
+2. Creating an HTTPServer for forwarding the traffic to this pipeline.
+
+``` bash
+echo '
+kind: HTTPServer
+name: server-demo
+port: 10080
+keepAlive: true
+https: false
+rules:
+  - paths:
+    - pathPrefix: /api
+      backend: pipeline-api | egctl object create
+
+```
+
+3. Request the aggregator pipeline
+
+``` bash
+
+$ curl  -X GET  http://127.0.0.1:10080/api -v
+[{"hello":"world"},{"hello":"new world"},{"mega":"ease"}]
+
+```
+
+### Scenario 2: Merge response body
+
+* As in #Scenario 1,  `pipeline-demo1` and `pipeline-demo2`'s responses share the same JSON key, we want to merge their response body by the JSON key together. If the keys have conflicted, we will use the last value.
+
+1. Update the pipeline with the aggregator
+
+``` bash
+echo '
+name: pipeline-api
+kind: HTTPPipeline
+flow:
+  - filter: agg
+filters:
+  - name: agg
+    kind: APIAggregator
+    mergeResponse: true   // turn on this switch
+    pipelines:
+      - name: pipeline-demo
+      - name: pipeline-demo1
+      - name: pipeline-demo2 '  | egctl object update
+```
+
+2. Request the aggregator pipeline
+
+``` bash
+
+$ curl  -X GET  http://127.0.0.1:10080/api -v
+{"hello":"new world","mega":"ease"}
+
+```
+
+### Scenario 3: Allow partial succeed
+
+* As In #Scenario 1, if the backend service meets some problem or can't provide service, this aggregator will
+fail as blew:
+
+``` bash
+$ curl http://localhost:10080/api -v -X PUT
+*   Trying ::1...
+* TCP_NODELAY set
+* Connected to localhost (::1) port 10080 (#0)
+> PUT /api HTTP/1.1
+> Host: localhost:10080
+> User-Agent: curl/7.64.1
+> Accept: */*
+>
+< HTTP/1.1 503 Service Unavailable
+< X-Eg-Aggregator: failed-in-pipeline-demo
+< Date: Wed, 28 Jul 2021 09:11:18 GMT
+< Content-Length: 0
+<
+* Connection #0 to host localhost left intact
+* Closing connection 0
+```
+
+* We can see an `X-Eg-Aggregator: failed-in-pipeline-demo` header in the response, that's the first met failure pipeline of this aggregator.
+
+* In some scenarios, we want this aggregator to return the successful execution pipelines' result. We can achieve this purpose by the steps below
+
+1. Update the aggregator's spec
+
+``` bash
+echo '
+name: pipeline-api
+kind: HTTPPipeline
+flow:
+  - filter: agg
+filters:
+  - name: agg
+    kind: APIAggregator
+    partialSucceed: true   // turn on this switch
+    pipelines:
+      - name:  pipeline-demo
+      - name:  pipeline-demo1
+      - name: pipeline-demo2 '  | egctl object update
+
+```
+
+2. Request the aggregator
+
+``` bash
+$ curl  -X GET  http://127.0.0.1:10080/api -v
+[{"hello":"world"},{"hello":"new world"}]
+
+```
+
+## References
+
+1. https://docs.microsoft.com/en-us/azure/architecture/patterns/gateway-aggregation
diff --git a/doc/cookbook/distributed_tracing.md b/doc/cookbook/distributed_tracing.md
new file mode 100644
index 00000000..61b50637
--- /dev/null
+++ b/doc/cookbook/distributed_tracing.md
@@ -0,0 +1,21 @@
+# Distributed Tracing
+
+We used [OpenTracing API](https://opentracing.io/) to build the tracing framework of Easegress. We officially support [Zipkin](https://zipkin.io/).  We can enable tracing in `HTTPServer`, it will start a span whose name is the same ahs the server. The matched pipeline will start a child span, and its internal filters will start children spans according to their implementation, for example, the `Proxy` did it.
+
+```yaml
+kind: HTTPServer
+name: http-server-example
+port: 10080
+tracing:
+  serviceName: httpServerExmple
+  zipkin:
+    hostport: 0.0.0.0:10080
+    serverURL: http://localhost:9412/api/v2/spans
+    sampleRate: 1
+    sameSpan: true
+    id128Bit: false
+rules:
+  - paths:
+    - pathPrefix: /pipeline
+      backend: http-pipeline-example
+```
\ No newline at end of file
diff --git a/doc/cookbook/faas.md b/doc/cookbook/faas.md
new file mode 100644
index 00000000..cca608d5
--- /dev/null
+++ b/doc/cookbook/faas.md
@@ -0,0 +1,236 @@
+# FaaS
+
+- [FaaS](#faas)
+  - [Background](#background)
+  - [Easegress works with FaaS](#easegress-works-with-faas)
+  - [Examples](#examples)
+    - [Scenario 1: Run a FaaS function beside Easegress](#scenario-1-run-a-faas-function-beside-easegress)
+    - [Scenario 2: Limit FaaS function resources using](#scenario-2-limit-faas-function-resources-using)
+    - [Scenario 3: Longlife FaaS function](#scenario-3-longlife-faas-function)
+    - [Scenario 4: Autoscaling FaaS Function according to rps](#scenario-4-autoscaling-faas-function-according-to-rps)
+  - [References](#references)
+    - [Resource limiter](#resource-limiter)
+    - [Long life function](#long-life-function)
+    - [RPS autoscaling](#rps-autoscaling)
+
+## Background
+
+* Function As Services is a recently popular cloud computing solution. It provides a platform for users to develop, run and manage application functionalities without the complexity of building and maintaining the infrastructure.[1]
+* Easegress provides a business controller for implementing these zero-infrastructure-maintaining and auto-scaling requirements.
+
+## Easegress works with FaaS
+
+* Isolation: separate Control logic and Business logic 
+* Traffic originated: Original near traffic, easier to integrate
+* Resource saving: reusing Easegress+K8s machine resources.
+* Pay what you used: reducing small customize features' developing and maintenance cost.
+
+## Examples
+
+
+### Scenario 1: Run a FaaS function beside Easegress
+
+* After implementing your business logic
+
+1. Create a FaaSController[2]
+
+``` bash
+echo 'name: faascontroller
+kind: FaaSController
+provider: knative             # FaaS provider kind, currently we only support Knative
+syncInterval: 10s
+
+httpServer:
+    http3: false
+    port: 10083
+    keepAlive: true
+    keepAliveTimeout: 60s
+    maxConnections: 10240
+
+knative:
+   networkLayerURL: http://${knative_kourier_clusterIP}
+   hostSuffix: example.com '| egctl object create
+```
+
+2. Deploy a function into Easegress and Knative, prepare a YAML content as below:
+
+``` yaml
+name:           "demo"
+image:          "${image_url}"
+port:           8089
+autoScaleType:  "concurrency"
+autoScaleValue: "100"
+```
+
+* Save it into `/home/easegress/function.yaml`, using command to deploy it in Easegress:
+**Note** this command should be run in Easegress' instance environment and ${eg_apiport} should be replaced with the real working port, ${image_url} should be replaced with pullable image URL.
+
+``` bash
+$ curl --data-binary @/home/easegress/function.yaml -X POST -H 'Content-Type: text/vnd.yaml' http://127.0.0.1:${eg_apiport}/apis/v1/faas/faascontroller
+```
+
+3. Get the function's status, make sure it is in `active` status
+
+``` bash
+$ curl http://127.0.0.1:${eg_apiport}/apis/v1/faas/faascontroller/demo
+spec:
+  name: demo
+  image: dev.local/demo:1.0
+  port: 8089
+  autoScaleType: rps
+  autoScaleValue: "1000"
+  minReplica: 0
+  maxReplica: 0
+  limitCPU:
+  limitMemory:
+  requestCPU:
+  requestMemory:
+  requestAdaptor:
+    host: ""
+    method: ""
+    header:
+      del: {}
+      set: {}
+      add: {}
+    body: ""
+status:
+  name: demo10
+  state: active
+  event: ready
+  extData: {}
+fsm: null
+```
+
+4. Request the FaaS function by Easegress HTTP traffic gate with `X-FaaS-Func-Name: demo` in the HTTP header.
+**Note**: this example HTTP backend's API works on `/tomcat/job/api` path and its business logic is echoing back your request body and with `V3 Body is ` content.
+
+``` bash
+$ curl http://127.0.0.1:10083/tomcat/job/api -H "X-FaaS-Func-Name: demo" -X POST -d ‘{"megaease":"Hello Easegress+Knative"}’
+V3 Body is
+‘{megaease:Hello Easegress+Knative}’%
+```
+
+### Scenario 2: Limit FaaS function resources using
+* You want to make sure at the maximum instance number can only be under 50, and it can only "180m" CPU and "100Mi" memory usage maximum allowed per instance. To providing meaningful resources amount for the function, you also want to make sure one instance has at least a "100m" CPU and "50mi" memory provision. (The CPU and memory limitation usage value comes from Kubernetes resource).
+
+``` yaml
+name: demo
+#...
+
+limitedMemory: "200Mi"
+limitedCPU: "180m"
+requireMemory: "100Mi"
+requireCPU: "100m"
+minReplica: 0
+maxReplica: 50
+
+```
+
+* For the full YAML, see [here](#resource-limiter)
+
+* Add the configuration above in #Scenario 1's `/home/easegress/function.yaml`
+
+1. Stop the function execution by using command
+
+``` bash
+$ curl http://127.0.0.1:${eg_apiport}/apis/v1/faas/faascontroller/demo/stop -X PUT
+```
+
+* The function will become `inactive` then we can update the resource limitation safely.
+
+2. Update the function's spec
+
+``` bash
+$ curl --data-binary @/home/easegress/function.yaml -X PUT -H 'Content-Type: text/vnd.yaml' http://127.0.0.1:${eg_apiport}/apis/v1/faas/faascontroller/demo
+```
+
+3. Verify the update
+* Waiting for the function starts successfully and becomes `active`
+* Request the function with step4 in Scenario 1.
+
+### Scenario 3: Longlife FaaS function
+* In the same special cases, you may want your FaaS function to have at least one instance running beside Easegress.
+
+``` yaml
+name: demo
+#...
+minReplica:  1
+#...
+```
+
+* For the full YAML, see [here](#long-life-function)
+
+1. Modifying the `minReplica` above in #Scenario 1's `/home/easegress/function.yaml`
+
+2. Update the function spec and verify it as in Scenario 2's steps 2 - 3.
+
+### Scenario 4: Autoscaling FaaS Function according to rps
+
+* If you don't need to control the function's allowed request precisely, `RPS` based autoscaling is a good choice.
+
+``` yaml
+name: demo
+#...
+autoScaleType:  "rps"
+autoScaleValue: "6000"
+#...
+```
+
+* For the full YAML, see [here](#rps-autoscaling)
+
+1. Modifying the `autoScaleType`  and `autoScaleValue" above in #Scenario 1's `/home/easegress/function.yaml`
+
+2. Update the function spec and verify it as in Scenario 2's step 2 - 3.
+
+## References
+
+1. https://en.wikipedia.org/wiki/Function_as_a_service
+2. https://github.com/megaease/easegress/blob/main/doc/faascontroller.md
+
+### Resource limiter
+
+```yaml
+name:           demo
+image:          "${image_url}"
+port:           8089
+autoScaleType:  "concurrency"
+autoScaleValue: "100
+limitedMemory: "200Mi"
+limitedCPU:    "180m"
+requireMemory: "100Mi"
+requireCPU:    "100m"
+minReplica:    0
+maxReplica:    50
+```
+
+### Long life function
+
+``` yaml
+name:           demo
+image:          "${image_url}"
+port:           8089
+autoScaleType:  "concurrency"
+autoScaleValue: "100"
+limitedMemory: "200Mi"
+limitedCPU:    "180m"
+requireMemory: "100Mi"
+requireCPU:    "100m"
+minReplica:    1 
+maxReplica:    50
+```
+
+### RPS autoscaling
+
+``` yaml
+name:           demo
+image:          "${image_url}"
+port:           8089
+autoScaleType:  "rps"
+autoScaleValue: "6000"
+limitedMemory: "200Mi"
+limitedCPU:    "180m"
+requireMemory: "100Mi"
+requireCPU:    "100m"
+minReplica:    0 
+maxReplica:    50
+```
\ No newline at end of file
diff --git a/doc/cookbook/flash_sale.md b/doc/cookbook/flash_sale.md
new file mode 100644
index 00000000..308bc11e
--- /dev/null
+++ b/doc/cookbook/flash_sale.md
@@ -0,0 +1,3 @@
+# Flash Sale
+
+Working in progress, coming soon.
diff --git a/doc/cookbook/k8s_ingress_controller.md b/doc/cookbook/k8s_ingress_controller.md
new file mode 100644
index 00000000..0a37a5c3
--- /dev/null
+++ b/doc/cookbook/k8s_ingress_controller.md
@@ -0,0 +1,191 @@
+# Kubernetes Ingress Controller
+
+- [Kubernetes Ingress Controller](#kubernetes-ingress-controller)
+  - [Why Use an Ingress Controller](#why-use-an-ingress-controller)
+  - [Cookbook](#cookbook)
+    - [Basic: Handle Ingresses from All K8s namespaces](#basic-handle-ingresses-from-all-k8s-namespaces)
+    - [Handle Ingresses within Specified K8s Namespaces](#handle-ingresses-within-specified-k8s-namespaces)
+    - [Use a Customized Ingress Class](#use-a-customized-ingress-class)
+    - [Deploy Outside of a K8s Cluster](#deploy-outside-of-a-k8s-cluster)
+
+The IngressController is an implementation of [Kubernetes ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/), it watches Kubernetes Ingress, Service, Endpoints, and Secrets then translates them to Easegress HTTP server and pipelines.
+
+This document list example configurations for typical scenarios, more details could be found at [the guide of ingress controller](./ingresscontroller.md).
+
+## Why Use an Ingress Controller
+
+* **Cuts Down Infrastructure Costs**: Without an ingress controller, when we wanted to expose 20 services to the public internet, we have to pay for 20 cloud load balancers; but with an ingress controller, we only need to pay for one cloud load balancer.
+* **Scalable Layer 7 Load Balancer**
+* **Manage Load Balancer Configuration in a Distributed Fashion**
+
+## Cookbook
+
+### Basic: Handle Ingresses from All K8s namespaces
+
+Create an ingress controller, it handles ingresses from all K8s namespaces when `namespaces` is an empty array.
+
+```bash
+echo '
+kind: IngressController
+name: ingress-controller-example
+namespaces: []                             # Keep the value an empty array
+httpServer:
+  port: 8080
+  https: false
+  keepAlive: true
+  keepAliveTimeout: 60s
+  maxConnections: 10240
+' | egctl object create
+```
+
+Create two versions of `hello` service in Kubernetes: 
+
+```bash
+echo '
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: hello-deployment
+spec:
+  selector:
+    matchLabels:
+      app: products
+      department: sales
+  replicas: 2
+  template:
+    metadata:
+      labels:
+        app: products
+        department: sales
+    spec:
+      containers:
+      - name: hello-v1
+        image: "gcr.io/google-samples/node-hello:1.0"
+        env:
+        - name: "PORT"
+          value: "50001"
+      - name: hello-v2
+        image: "gcr.io/google-samples/hello-app:2.0"
+        env:
+        - name: "PORT"
+          value: "50002"
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: hello-service
+spec:
+  type: NodePort
+  selector:
+    app: products
+    department: sales
+  ports:
+  - name: port_v1
+    protocol: TCP
+    port: 60001
+    targetPort: 50001
+  - name: port_v2
+    protocol: TCP
+    port: 60002
+    targetPort: 50002
+' | kubectl apply 
+```
+
+Create a Kubernetes ingress for the two services, note the `ingressClassName` is `easegress`:
+
+```bash
+echo '
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-example
+spec:
+  ingressClassName: easegress
+  rules:
+  - host: "www.example.com"
+    http:
+      paths:
+      - pathType: Prefix
+        path: /
+        backend:
+          service:
+            name: hello-service
+            port:
+              number: 60001
+  - host: "*.megaease.com"
+    http:
+      paths:
+      - pathType: Prefix
+        path: /
+        backend:
+          service:
+            name: hello-service
+            port:
+              number: 60002
+' | kubectl apply 
+```
+
+After a while, we can leverage the below command to access both versions of the `hello` application:
+
+```bash
+$ curl http://{NODE_IP}/ -HHost:www.megaease.com
+Hello, world!
+Version: 2.0.0
+Hostname: hello-deployment-6cbf765985-r6242
+
+$ curl http://{NODE_IP}/ -HHost:www.example.com
+Hello Kubernetes!
+```
+
+### Handle Ingresses within Specified K8s Namespaces
+
+When one or more K8s namespaces are listed in `namespaces`, the ingress controller only handles ingresses within these namespaces.
+
+```yaml
+kind: IngressController
+name: ingress-controller-example
+namespaces: ['sales', 'customer']          # List namespaces here
+httpServer:
+  port: 8080
+  https: false
+  keepAlive: true
+  keepAliveTimeout: 60s
+  maxConnections: 10240
+```
+
+### Use a Customized Ingress Class
+
+By default, the ingress controller handles ingresses with `IngressClassName` set to `easegress`, this can be changed by specifying the `ingressClass` in configuration.
+
+```yaml
+kind: IngressController
+name: ingress-controller-example
+namespaces: []
+ingressClass: myingress                    # specify the target ingress class
+httpServer:
+  port: 8080
+  https: false
+  keepAlive: true
+  keepAliveTimeout: 60s
+  maxConnections: 10240
+```
+
+### Deploy Outside of a K8s Cluster
+
+When deployed outside of a K8s Cluster, we must specify `kubeConfig` or `masterURL` or both of them.
+
+```yaml
+kind: IngressController
+name: ingress-controller-example
+kubeConfig: /home/megaease/.kube/config    # Path of the K8s configuration file
+masterURL: http://localhost:8080/api/      # URL of the K8s API server
+namespaces: []
+httpServer:
+  port: 8080
+  https: false
+  keepAlive: true
+  keepAliveTimeout: 60s
+  maxConnections: 10240
+```
diff --git a/doc/cookbook/load_balancer.md b/doc/cookbook/load_balancer.md
new file mode 100644
index 00000000..0855330f
--- /dev/null
+++ b/doc/cookbook/load_balancer.md
@@ -0,0 +1,121 @@
+# Load Balancer
+
+- [Load Balancer](#load-balancer)
+  - [Why Use Easegress as Reverse Proxy](#why-use-easegress-as-reverse-proxy)
+  - [Basic: Load Balance](#basic-load-balance)
+  - [Traffic Adaptor: Change Something of Two-Way Traffic](#traffic-adaptor-change-something-of-two-way-traffic)
+  - [References](#references)
+    - [Traffic Adaptor: Change Something of Two-Way Traffic](#traffic-adaptor-change-something-of-two-way-traffic-1)
+
+The reverse proxy is the common middleware that is accessed by clients, forwards them to backend servers. The reverse proxy is a very core role played by Easegress.
+
+## Why Use Easegress as Reverse Proxy
+
+Easegress integrates many features as a reverse proxy with easy configuration.
+
+- Offload SSL/TLS layer
+- Hot-updated routing without losing requests
+- Intercept invalid requests (according to IP, metadata of requests, credential verification, etc)
+- Adapting requests to satisfy requirements of backend servers
+- Bring more resilience for backend servers (rate limiting, time limiting, circuit breaker, retrying)
+- Load balance for backend servers (static addresses, or integration with service registry)
+- Compression and Caching for response
+
+## Basic: Load Balance
+
+The filter `Proxy` is the filter to fire requests to backend servers. It contains servers group under load balance, whose policy support `roundRobin`, `random`, `weightedRandom`, `ipHash`, `headerHash`.
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+## Traffic Adaptor: Change Something of Two-Way Traffic
+
+Sometimes backend applications can't adapt to quick changes of requirements of traffic. Easegress could be an adaptor between new traffic and old applications. There are 2 phases of adaption in reverse proxy: request adaption, response adaption. `RequestAdaptor` supports the adaption of a method, path, header, and body. `ResponseAdaptor` supports the adaption of header and body. As you can see, the flow in spec plays a critical role.
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: requestAdaptor
+  - fitter: proxy
+  - filter: responseAdaptor
+filters:
+  - name: requestAdaptor
+    kind: RequestAdaptor
+    host: easegress.megaease.com
+    method: POST
+    path:
+    addPrefix: /apis/v2
+    header:
+      set:
+        X-Api-Version: v2
+
+  - name: responseAdaptor
+    kind: ResponseAdaptor
+    header:
+      set:
+        Server: Easegress v1.0.0
+      add:
+        X-Easegress-Pipeline: pipeline-reverse-proxy
+
+  - name: proxy
+    kind: Proxy
+    # ...
+```
+
+For the full YAML, see [here](#traffic-adaptor-change-something-of-two-way-traffic-1)
+
+## References
+
+### Traffic Adaptor: Change Something of Two-Way Traffic
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: requestAdaptor
+  - fitter: proxy
+  - filter: responseAdaptor
+filters:
+  - name: requestAdaptor
+    kind: RequestAdaptor
+    host: easegress.megaease.com
+    method: POST
+    path:
+    addPrefix: /apis/v2
+    header:
+      set:
+        X-Api-Version: v2
+
+  - name: responseAdaptor
+    kind: ResponseAdaptor
+    header:
+      set:
+        Server: Easegress v1.0.0
+      add:
+        X-Easegress-Pipeline: pipeline-reverse-proxy
+
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
diff --git a/doc/cookbook/performance.md b/doc/cookbook/performance.md
new file mode 100644
index 00000000..63a1f626
--- /dev/null
+++ b/doc/cookbook/performance.md
@@ -0,0 +1,192 @@
+# Performance
+
+- [Performance](#performance)
+  - [Basic: Load Balance](#basic-load-balance)
+  - [Performance: Compression and Caching](#performance-compression-and-caching)
+    - [Compression in filter `Proxy`](#compression-in-filter-proxy)
+    - [Caching in filter `Proxy`](#caching-in-filter-proxy)
+    - [Caching in `HTTPServer`](#caching-in-httpserver)
+  - [References](#references)
+    - [Proxy Compression](#proxy-compression)
+    - [Proxy Caching](#proxy-caching)
+    - [HTTPServer route rule caching](#httpserver-route-rule-caching)
+
+Easegress supports compression and caching in HTTPPipeline and HTTPServer for improving performance purposes as a revert proxy.
+
+
+## Basic: Load Balance
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+## Performance: Compression and Caching
+
+### Compression in filter `Proxy`
+
+* Easegress proxy filter supports `gzip` type compression. It can save the bandwidth between the client and Easegress and reduce the time cost.
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+#...
+    compression:
+      minLength: 1024
+#...
+```
+
+* As the example above, we only need to value the `minLength` field to tell Easegress' proxy filter compressing response body whose size is bigger than `minLength`. Also, it will add the `gzip` header automatically if it's truly compressed.
+
+* For the full yaml, see [here](#proxy-compression)
+
+### Caching in filter `Proxy`
+
+* Easegress proxy filter has a `pool` section for describing the traffic forwarding backends. And it also supports caching the response according to the HTTP Methods and the HTTP response code. **Recommend enabling this feature only in the routing static resources scenario**.
+
+``` yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+#...
+      memoryCache:
+        expiration: 10s
+        maxEntryBytes: 4096
+        codes:
+        - 200
+        - 201
+        methods:
+        - GET
+        - HEAD
+#...
+```
+
+* The example above will cache the response which size is smaller than 4096, and the response code is 200 or 201, with the HTTP methods Get and Head.
+
+* For the full YAML, see [here](#proxy-caching)
+
+### Caching in `HTTPServer`
+
+* As a traffic gate of Easegress, HTTPServer also supports caching routing rules. It reduces the HTTPServer route searching cost. HTTPServer will use the request's Host, Method, and Path to form a key for the build-in LRU rule cache.
+
+```yaml
+kind: HTTPServer
+name: http-server-example
+port: 10080
+https: false
+#...
+cacheSize: 10240
+#...
+```
+
+* As the example above, all we need is to set the `cacheSize` to indicated the LRU cache's size. It will disuse the least used cache rules firstly.
+
+* For the full YAML, see [here](#httpserver-route-rule-caching)
+
+## References
+### Proxy Compression
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+     compression:
+       minLength: 1024
+```
+
+### Proxy Caching
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+      memoryCache:
+        expiration: 10s
+        maxEntryBytes: 4096
+        codes:
+        - 200
+        - 201
+        methods:
+        - GET
+        - HEAD
+```
+
+### HTTPServer route rule caching
+
+``` yaml
+kind: HTTPServer
+name: http-server-example
+port: 10080
+https: false
+http3: false
+keyBase64:
+certBase64:
+keepAlive: true
+keepAliveTimeout: 75s
+maxConnection: 10240
+cacheSize: 10240
+rules:
+  - paths:
+    - pathPrefix: /pipeline
+      backend: http-pipeline-example
+    - pathPrefix: /remote
+      backend: remote-pipeline
+      headers:
+      - key: X-Activity-No
+        values: [ "123456", "124456" ]
+        backend: remote-pipeline1
+      - key: X-Activity-No
+        values: [ "224456" ]
+        regexp: ^224.*$
+        backend: remote-pipeline2
+      - key: X-Activity-No
+        regexp: ^324.*$
+        backend: remote-pipeline2
+    - path: /func
+      backend: func-mirror
+
+```
\ No newline at end of file
diff --git a/doc/cookbook/resilience.md b/doc/cookbook/resilience.md
new file mode 100644
index 00000000..ba04bfd5
--- /dev/null
+++ b/doc/cookbook/resilience.md
@@ -0,0 +1,364 @@
+# Resilience
+
+- [Resilience](#resilience)
+  - [Basic: Load Balance](#basic-load-balance)
+  - [More Livingness: Resilience of Service](#more-livingness-resilience-of-service)
+    - [CircuitBreaker](#circuitbreaker)
+    - [RateLimiter](#ratelimiter)
+    - [Retryer](#retryer)
+    - [TimeLimiter](#timelimiter)
+  - [References](#references)
+    - [CircuitBreaker](#circuitbreaker-1)
+    - [RateLimiter](#ratelimiter-1)
+    - [Retryer](#retryer-1)
+    - [TimeLimiter](#timelimiter-1)
+    - [Concepts](#concepts)
+
+The as a Cloud Native traffic orchestrator, Easegress supports build-in resilience features. It is the ability of your system to react to failure and still remain functional. It's not about avoiding failure, but accepting failure and constructing your cloud-native services to respond to it. You want to return to a fully functioning state quickly as possible.[1]
+
+## Basic: Load Balance
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+## More Livingness: Resilience of Service
+
+### CircuitBreaker
+
+Below is an example configuration with a `COUNT_BASED` policy. `GET` request to paths begin with `/books/` uses this policy, which short-circuits requests if more than half of the last 100 requests failed with status code 500, 503, or 504.
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: circuit-breaker
+  - filter: proxy
+filters:
+  - name: circuit-breaker
+    kind: CircuitBreaker
+    policies:
+    - name: count-based-policy
+      slidingWindowType: COUNT_BASED
+      failureRateThreshold: 50
+      slidingWindowSize: 100
+      failureStatusCodes: [500, 503, 504]
+    urls:
+    - methods: [GET]
+      url:
+        prefix: /books/
+      policyRef: count-based-policy
+  - name: proxy
+    kind: Proxy
+```
+
+And we can add a `TIME_BASED` policy, `GET` & `POST` requests to paths that match regular express `^/users/\d+$` uses this policy, which short-circuits requests if more than 60% of the requests within the last 200 seconds failed.
+
+```yaml
+    policies:
+    - name: time-based-policy
+      slidingWindowType: TIME_BASED
+      failureRateThreshold: 60
+      slidingWindowSize: 200
+      failureStatusCodes: [500, 503, 504]
+    urls:
+    - methods: [GET, POST]
+      url:
+        regex: ^/users/\d+$
+      policyRef: time-based-policy
+```
+
+In addition to failures, the circuit breaker can also short-circuit requests on slow requests. Below configuration regards requests which cost more than 30 seconds as slow requests and short-circuits requests if 60% of recent requests are slow.
+
+```yaml
+    policies:
+    - name: count-based-policy
+      slowCallRateThreshold: 60
+      slowCallDurationThreshold: 30s
+```
+
+For a policy, if the first request fails, the failure rate could be 100% because there's only one request. This is not the desired behavior in most cases, we can avoid it by specifying `minimumNumberOfCalls`.
+
+```yaml
+    policies:
+    - name: count-based-policy
+      minimumNumberOfCalls: 10
+```
+
+We can also configure the wait duration in the `open` state and the max wait duration in the `half-open` state:
+
+```yaml
+    policies:
+    - name: count-based-policy
+      waitDurationInOpenState: 2m
+      maxWaitDurationInHalfOpenState: 1m
+```
+
+In the `half-open` state, we can limit the number of permitted requests:
+
+```yaml
+    policies:
+    - name: count-based-policy
+      permittedNumberOfCallsInHalfOpenState: 10
+```
+
+For the full YAML, see [here](#circuitbreaker-1)
+
+### RateLimiter
+
+> NOTE: When there are multiple instances of Easegress, the configuration will be applied for every instance equally. For example, TPS of RateLimiter is configured with 100 in 3-instances cluster, so the total TPS will be 300.
+
+The below configuration limits the request rate for requests to `/admin` and requests that match regular expression `^/pets/\d+$`.
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: rate-limiter
+  - filter: proxy
+filters:
+  - name: rate-limiter
+    kind: RateLimiter
+    policies:
+    - name: policy-example
+      timeoutDuration: 100ms
+      limitRefreshPeriod: 10ms
+      limitForPeriod: 50
+    defaultPolicyRef: policy-example
+    urls:
+    - methods: [GET, POST, PUT, DELETE]
+      url:
+        exact: /admin
+        regex: ^/pets/\d+$
+      policyRef: policy-example
+  - name: proxy
+    kind: Proxy
+```
+
+For the full YAML, see [here](#ratelimiter-1)
+
+### Retryer
+
+If we want to retry on HTTP status code 500, 503, and 504, we can create a `Retryer` with the below configuration, it makes at most 3 attempts on failure.
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: retryer
+  - filter: proxy
+filters:
+  - name: retryer
+    kind: Retryer
+    policies:
+    - name: policy-example
+      maxAttempts: 3
+      waitDuration: 500ms
+      failureStatusCodes: [500, 503, 504]
+    defaultPolicyRef: policy-example
+    urls:
+    - methods: [GET, POST, PUT, DELETE]
+      url:
+        prefix: /books/
+      policyRef: policy-example
+  - name: proxy
+    kind: Proxy
+```
+
+By default, the wait duration between two attempts is `waitDuration`, but this can be changed by specifying `backOffPolicy` and `randomizationFactor`.
+
+```yaml
+    - name: policy-example
+      backOffPolicy: Exponential
+      randomizationFactor: 0.5
+```
+
+For the full YAML, see [here](#retryer-1)
+
+### TimeLimiter
+
+TimeLimiter limits the time of requests, a request is canceled if it cannot get a response in configured duration.
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: time-limiter
+  - filter: proxy
+filters:
+  - name: time-limiter
+    kind: TimeLimiter
+    defaultTimeoutDuration: 500ms
+    urls:
+    - methods: [POST]
+      url:
+        exact: /users/1
+      timeoutDuration: 400ms
+  - name: proxy
+    kind: Proxy
+```
+
+For the full YAML, see [here](#timelimiter-1)
+
+## References
+
+### CircuitBreaker
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: circuit-breaker
+  - filter: proxy
+filters:
+  - name: circuit-breaker
+    kind: CircuitBreaker
+    policies:
+    - name: count-based-policy
+      slidingWindowType: COUNT_BASED
+      failureRateThreshold: 50
+      slidingWindowSize: 100
+      failureStatusCodes: [500, 503, 504]
+      slowCallRateThreshold: 60
+      slowCallDurationThreshold: 30s
+      minimumNumberOfCalls: 10
+      waitDurationInOpenState: 2m
+      maxWaitDurationInHalfOpenState: 1m
+      permittedNumberOfCallsInHalfOpenState: 10
+    - name: time-based-policy
+      slidingWindowType: TIME_BASED
+      failureRateThreshold: 60
+      slidingWindowSize: 200
+      failureStatusCodes: [500, 503, 504]
+    urls:
+    - methods: [GET]
+      url:
+        prefix: /books/
+      policyRef: count-based-policy
+    - methods: [GET, POST]
+      url:
+        regex: ^/users/\d+$
+      policyRef: time-based-policy
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+### RateLimiter
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: rate-limiter
+  - filter: proxy
+filters:
+  - name: rate-limiter
+    kind: RateLimiter
+    policies:
+    - name: policy-example
+      timeoutDuration: 100ms
+      limitRefreshPeriod: 10ms
+      limitForPeriod: 50
+    defaultPolicyRef: policy-example
+    urls:
+    - methods: [GET, POST, PUT, DELETE]
+      url:
+        exact: /admin
+        regex: ^/pets/\d+$
+      policyRef: policy-example
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+### Retryer
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: retryer
+  - filter: proxy
+filters:
+  - name: retryer
+    kind: Retryer
+    policies:
+    - name: policy-example
+      backOffPolicy: Exponential
+      randomizationFactor: 0.5
+      maxAttempts: 3
+      waitDuration: 500ms
+      failureStatusCodes: [500, 503, 504]
+    defaultPolicyRef: policy-example
+    urls:
+    - methods: [GET, POST, PUT, DELETE]
+      url:
+        prefix: /books/
+      policyRef: policy-example
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+### TimeLimiter
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: time-limiter
+  - filter: proxy
+filters:
+  - name: time-limiter
+    kind: TimeLimiter
+    defaultTimeoutDuration: 500ms
+    urls:
+    - methods: [POST]
+      url:
+        exact: /users/1
+      timeoutDuration: 400ms
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+### Concepts
+1. https://docs.microsoft.com/en-us/dotnet/architecture/cloud-native/resiliency
diff --git a/doc/cookbook/security.md b/doc/cookbook/security.md
new file mode 100644
index 00000000..5612b6ac
--- /dev/null
+++ b/doc/cookbook/security.md
@@ -0,0 +1,257 @@
+# Security
+
+- [Security](#security)
+  - [Basic: Load Balance](#basic-load-balance)
+  - [Security: Verify Credential](#security-verify-credential)
+    - [Header](#header)
+    - [JWT](#jwt)
+    - [Signature](#signature)
+    - [OAuth2](#oauth2)
+  - [References](#references)
+    - [Header](#header-1)
+    - [JWT](#jwt-1)
+    - [Signature](#signature-1)
+    - [OAuth2](#oauth2-1)
+    - [Concepts](#concepts)
+
+As a production-ready cloud-native traffic orchestrator, Easegress cares about security and provides several features to ensure that.
+
+## Basic: Load Balance
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+## Security: Verify Credential
+
+### Header
+
+* Using Headers validation in Easegress. This is the simplest way for validating requests. It checks the HTTP request headers by using regular expression matching.
+
+``` yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: header-validator
+  - filter: proxy
+filters:
+  - kind: Validator
+    name: header-validator
+    headers:
+      Is-Valid:
+        values: ["abc", "goodplan"]
+        regexp: "^ok-.+$"
+  - name: proxy
+    kind: Proxy
+```
+
+* To enable Header type validator correctly in the pipeline, we should add it before filter `Proxy`.
+* As the example above, it will check the `Is-Valid` header field by trying to match `abc` or `goodplan`. Also, it will use `^ok-.+$` regular expression for checking if it can't match the `values` filed.
+
+*For the full YAML, see [here](#header-1)
+
+### JWT
+
+* Using JWT validation in Easegress. JWT is wildly used in the modern web environment. JSON Web Token (JWT, pronounced /dʒɒt/, same as the word "jot") is a proposed Internet standard for creating data with optional signature and/or optional encryption whose payload holds JSON that asserts some number of claims.[1]
+> Easegress supports three types of JWT, HS256, HS384, and HS512.
+
+
+``` yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: jwt-validator
+  - filter: proxy
+filters:
+  - kind: Validator
+    name: jwt-validator
+    jwt:
+      cookieName: auth
+      algorithm: HS256
+      secret: 6d79736563726574
+  - name: proxy
+    kind: Proxy
+```
+
+The example above will check the value named `auth` in the cookie with HS256 with the secret,6d79736563726574.
+For the full YAML, see [here](#jwt-1)
+
+### Signature
+* Using Signature validation in Easegress. Signature validation implements an Amazon Signature V4[2] compatible signature validation validator. Once you enable this kind of validation, please make sure your HTTP client has followed the signature generation process in AWS V4 doc and bring it to request Easegress.
+
+``` yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: signature-validator
+  - filter: proxy
+filters:
+  - kind: Validator
+    name: signature-validator
+    signature:
+      accessKeys:
+        AKID: SECRET
+  - name: proxy
+    kind: Proxy
+
+```
+
+The example here only uses an `accessKeys` for processing Amazon Signature V4 validation. It also has other complicated and customized fields for more security purposes. Check it out in the Easegress filter doc if needed.[3]
+
+For the full YAML, see [here](#signature-1)
+
+### OAuth2
+
+* Using OAuth2 validation in Easegress. OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices. This specification and its extensions are being developed within the IETF OAuth Working Group.[4]
+
+``` yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: oauth-validator
+  - filter: proxy
+filters:
+  - kind: Validator
+    name: oauth-validator
+    oauth2:
+      tokenIntrospect:
+      endPoint: https://127.0.0.1:8443/auth/realms/test/protocol/openid-connect/token/introspect
+      clientId: easegress
+      clientSecret: 42620d18-871d-465f-912a-ebcef17ecb82
+      insecureTls: false
+  - name: proxy
+    kind: Proxy
+
+```
+
+* The example above uses a token introspection server, which is provided by `endpoint` filed for validation. It also supports `Self-Encoded Access Tokens mode` which will require a JWT related configuration included. Check it out in the Easegress filter doc if needed. [5]
+
+* For the full YAML, see [here](#oauth-1)
+
+## References
+
+### Header
+
+``` yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: header-validator
+  - filter: proxy
+filters:
+  - kind: Validator
+    name: header-validator
+    headers:
+      Is-Valid:
+        values: ["abc", "goodplan"]
+        regexp: "^ok-.+$"
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+
+```
+
+### JWT
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: jwt-validator
+  - filter: proxy
+filters:
+  - kind: Validator
+    name: jwt-validator
+    jwt:
+      cookieName: auth
+      algorithm: HS256
+      secret: 6d7973656372657
+       - name: proxy
+  - kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+### Signature
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: signature-validator
+  - filter: proxy
+filters:
+  - kind: Validator
+    name: signature-validator
+    signature:
+      accessKeys:
+        AKID: SECRET
+  - kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+### OAuth2
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: oauth-validator
+  - filter: proxy
+filters:
+  - kind: Validator
+    name: oauth-validator
+    oauth2:
+      tokenIntrospect:
+      endPoint: https://127.0.0.1:8443/auth/realms/test/protocol/openid-connect/token/introspect
+      clientId: easegress
+      clientSecret: 42620d18-871d-465f-912a-ebcef17ecb82
+      insecureTls: false
+  - kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+
+### Concepts
+
+1. https://en.wikipedia.org/wiki/JSON_Web_Token
+2. https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
+3. https://github.com/megaease/easegress/blob/main/doc/filters.md#signerliteral
+4. https://oauth.net/2/
+5. https://github.com/megaease/easegress/blob/main/doc/filters.md#validatorOAuth2JWT
diff --git a/doc/cookbook/service_proxy.md b/doc/cookbook/service_proxy.md
new file mode 100644
index 00000000..d93e0d40
--- /dev/null
+++ b/doc/cookbook/service_proxy.md
@@ -0,0 +1,138 @@
+# Service Proxy
+
+- [Service Proxy](#service-proxy)
+  - [Basic: Load Balance](#basic-load-balance)
+    - [Dynamic: Integration with Service Registry](#dynamic-integration-with-service-registry)
+    - [Zookeeper](#zookeeper)
+    - [Consul](#consul)
+    - [Eureka](#eureka)
+
+Easegress servers as a reverse proxy. It can easily integrate with mainstream Service Registries.  
+
+
+## Basic: Load Balance
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      loadBalance:
+        policy: roundRobin
+```
+
+
+### Dynamic: Integration with Service Registry
+
+* We integrate `Proxy` with service registry such as [Consul](https://github.com/megaease/easegress/blob/main/doc/controllers.md#consulserviceregistry), [Etcd](https://github.com/megaease/easegress/blob/main/doc/controllers.md#etcdserviceregistry), [Zookeeper](https://github.com/megaease/easegress/blob/main/doc/controllers.md#zookeeperserviceregistry), [Eureka](https://github.com/megaease/easegress/blob/main/doc/controllers.md#eurekaserviceregistry). You need to create one of them to connect the external service registry. The service registry config takes higher priority than static servers. If the dynamic servers pulling failed, it will use static servers if there are.
+
+
+### Zookeeper
+
+1. First we need to create a ZookeeperServiceRegistry in Easegress 
+
+``` yaml
+kind: ZookeeperServiceRegistry
+name: zookeeper-001
+zkservices: [127.0.0.1:2181]
+prefix: /services
+conntimeout: 6s
+syncInterval: 10s
+```
+
+2. Create a pipeline and set its `serviceRegisty` field into `zookeeper-001` and it will look up the zookeeper configuration for the service named `springboot-application-order` as in filed `serviceName`.    
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      serviceRegisrty: zookeeper-001              # +
+      serviceName: springboot-application-order   # +
+      loadBalance:
+        policy: roundRobin
+```
+
+
+### Consul 
+
+1. First we need to create a ConsulServiceRegistry in Easegress 
+
+```yaml
+kind: ConsulServiceRegistry
+name: consul-001
+address: '127.0.0.1:8500'
+scheme: http
+syncInterval: 10s
+```
+
+2. Create a pipeline and set its `serviceRegisty` field into `consul-001` and it will look up the consul configuration for the service named `springboot-application-order` as in filed `serviceName`.    
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      serviceRegisrty: consul-001              # +
+      serviceName: springboot-application-order   # +
+      loadBalance:
+        policy: roundRobin
+```
+
+
+### Eureka
+1. First we need to create a EurekaServiceRegistry in Easegress 
+
+```yaml
+kind: EurekaServiceRegistry
+name: eureka-001
+endpoints: ['http://127.0.0.1:8761/eureka']
+syncInterval: 10s
+```
+
+
+2. Create a pipeline and set its `serviceRegisty` field into `consul-001` and it will look up the consul configuration for the service named `springboot-application-order` as in filed `serviceName`.    
+
+```yaml
+name: pipeline-reverse-proxy
+kind: HTTPPipeline
+flow:
+  - filter: proxy
+filters:
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://127.0.0.1:9095
+      - url: http://127.0.0.1:9096
+      - url: http://127.0.0.1:9097
+      serviceRegisrty: eureka-001              # +
+      serviceName: springboot-application-order   # +
+      loadBalance:
+        policy: roundRobin
+```
+
diff --git a/doc/cookbook/wasm.md b/doc/cookbook/wasm.md
new file mode 100644
index 00000000..0d90fbad
--- /dev/null
+++ b/doc/cookbook/wasm.md
@@ -0,0 +1,393 @@
+# WasmHost
+
+- [WasmHost](#wasmhost)
+	- [Why Use WasmHost](#why-use-wasmhost)
+	- [Examples](#examples)
+		- [Basic: Noop](#basic-noop)
+		- [Add a New Header](#add-a-new-header)
+		- [Add a New Header According to Configuration](#add-a-new-header-according-to-configuration)
+		- [Add a Cookie](#add-a-cookie)
+		- [Mock Response](#mock-response)
+		- [Return a Result Other Than 0](#return-a-result-other-than-0)
+
+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](https://webassembly.org/) 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:
+
+```bash
+$ make build_server GOTAGS=wasmhost
+```
+
+We will use [AssemblyScript](https://www.assemblyscript.org/) as the language of the examples, please ensure a recent version of [Git](https://git-scm.com/), [Golang](https://golang.org), [Node.js](https://nodejs.org/) and its package manager [npm](https://www.npmjs.com/) 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.
+
+1. Clone git repository `easegress-assemblyscript-sdk` to someware on disk
+
+	```bash
+	$ git clone https://github.com/megaease/easegress-assemblyscript-sdk.git
+	```
+
+2. Switch to a new directory and initialize a new node module:
+
+	```bash
+	npm init
+	```
+
+3. Install the AssemblyScript compiler using npm, assume that the compiler is not required in production and make it a development dependency:
+
+	```bash
+	npm install --save-dev assemblyscript
+	```
+
+4. 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:
+
+	```bash
+	npx asinit .
+	```
+
+5. Add `--use abort=` to the `asc` in `package.json`, for example:
+
+	```json
+	"asbuild:untouched": "asc assembly/index.ts --target debug --use abort=",
+	"asbuild:optimized": "asc assembly/index.ts --target release --use abort=",
+	```
+
+6. Replace the content of `assembly/index.ts` with the code below, note to replace `{EASEGRESS_SDK_PATH}` with the path in step 1:
+
+	```typescript
+	// 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 problem, 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)
+	})
+	```
+
+7. Build with the below command, if everything is right, `untouched.wasm` (the debug version) and `optimized.wasm` (the release version) will be generated at the `build` folder.
+   
+   ```bash
+   $ npm run asbuild
+   ```
+
+8. Create an HTTPServer in Easegress to listen on port 10080 to handle the HTTP traffic:
+
+	```bash
+	$ echo '
+	kind: HTTPServer
+	name: server-demo
+	port: 10080
+	keepAlive: true
+	https: false
+	rules:
+	- paths:
+	  - pathPrefix: /pipeline
+	    backend: wasm-pipeline' | egctl object create
+	```
+
+9. Create pipeline `wasm-pipeline` which includes a `WasmHost` filter:
+
+	```bash
+	$ echo '
+	name: wasm-pipeline
+	kind: HTTPPipeline
+	flow:
+	- filter: wasm
+	- filter: proxy
+	  jumpIf: { fallback: END }
+
+	filters:
+	- name: wasm
+	  kind: WasmHost
+	  maxConcurrency: 2
+	  code: /home/megaease/example/build/optimized.wasm
+	  timeout: 100ms
+	- name: proxy
+	  kind: Proxy
+	  mainPool:
+	    servers:
+	    - url: http://127.0.0.1:9095
+	    loadBalance:
+	      policy: roundRobin' | egctl object create
+	```
+
+	Note to replace `/home/megaease/example/build/optimized.wasm` with the path of the file generated in step 7.
+
+10. Set up a backend service at `http://127.0.0.1:9095` with the `mirror` server from the Easegress repository. Using another HTTP server is fine, but the `mirror` server prints requests it received to the console, which makes it much easier to verify the results:
+
+	```bash
+	$ git clone https://github.com/megaease/easegress.git
+	$ cd easegress
+	$ go run example/backend-service/mirror/mirror.go
+	```
+
+11. Execute below command in a new console to verify what we have done.
+
+	```bash
+	$ 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
+
+```typescript
+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:
+
+```bash
+$ npm run asbuild
+$ egctl wasm update
+$ 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
+
+```typescript
+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:
+
+```yaml
+filters:
+  - name: wasm
+    kind: WasmHost
+    parameters:                                        # +
+      headerName: "Wasm-Added-2"                       # +
+      headerValue: "I was added by WebAssembly too"    # +
+```
+
+Build and verify with:
+
+```bash
+$ npm run asbuild
+$ egctl wasm update
+$ 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
+
+```typescript
+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:
+
+```bash
+$ npm run asbuild
+$ egctl wasm update
+$ 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
+
+```typescript
+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:
+
+```bash
+$ echo '
+name: wasm-pipeline
+kind: HTTPPipeline
+flow:
+- filter: wasm
+filters:
+- name: wasm
+	kind: WasmHost
+	maxConcurrency: 2
+	code: /home/megaease/example/build/optimized.wasm
+	timeout: 100ms' | egctl object update wasm-pipeline
+```
+
+Build and verify with:
+
+```bash
+$ npm run asbuild
+$ egctl wasm update
+$ curl http://127.0.0.1:10080/pipeline -d 'Hello, Easegress'
+I have a new body now
+```
+
+### Return a Result Other Than 0
+
+```typescript
+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:
+
+```yaml
+flow:
+  - filter: wasm
+    jumpIf: { wasmResult1: END }                   # +
+  - filter: proxy
+```
+
+Build and verify with:
+
+```bash
+$ 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
+```
diff --git a/doc/cookbook/workflow.md b/doc/cookbook/workflow.md
new file mode 100644
index 00000000..cddfa74d
--- /dev/null
+++ b/doc/cookbook/workflow.md
@@ -0,0 +1,233 @@
+# Workflow
+
+- [Workflow](#workflow)
+  - [Background](#background)
+  - [Sequence workflow with HTTPTextTemplate](#sequence-workflow-with-httptexttemplate)
+  - [Example](#example)
+    - [Step 1: Create NBA API pipeline](#step-1-create-nba-api-pipeline)
+    - [Step 2: Create Fun translator API pipeline](#step-2-create-fun-translator-api-pipeline)
+    - [Step 3: Create orchestrion pipeline](#step-3-create-orchestrion-pipeline)
+    - [Step 4: Create HTTPServer for routing](#step-4-create-httpserver-for-routing)
+    - [Step 5: See the result](#step-5-see-the-result)
+  - [References](#references)
+
+## Background
+
+* A workflow consists of an orchestrated and repeatable pattern of activity, enabled by the systematic organization of resources into processes that transform materials, provide services, or process information. It can be depicted as a sequence of operations, the work of a person or group, the work of an organization of staff, or one or more simple or complex mechanisms.[1]
+* Easegress currently has a build-in sequence workflow in Pipeline. Furthermore, we also provide a template mechanism for more advanced usage.
+
+## Sequence workflow with HTTPTextTemplate
+
+* Already executed filter's metadata can be the input for next filter.
+* Orchestrating pipeline with APIAggregator, RequestAdaptor, and ResponseAdaptor.
+
+
+## Example
+* We use the free, fun, and open RESTful APIs to achieve this example.[2]
+* API1: NBA list, http://www.balldontlie.io/api/v1/players, its response is a list for all player's informactions.
+```
+{
+   "data":[
+      {
+         "height_inches":null,
+         "last_name":"Anigbogu",
+         "position":"C",
+         "team":{
+            "id":12,
+            "abbreviation":"IND",
+            "city":"Indiana",
+            "conference":"East",
+            "division":"Central",
+            "full_name":"Indiana Pacers",
+            "name":"Pacers"
+         },
+         "weight_pounds":null,
+         "id":14,
+         "first_name":"Ike",
+         "height_feet":null
+      },
+      {
+         "last_name":"Baker",
+         "position":"G",
+         "team":{
+            "id":20,
+            "abbreviation":"NYK",
+            ...
+         },
+         ...
+      },
+   ]
+}
+```
+* API2: Fun translator, http://api.funtranslations.com/translate/minion.json, its response body will be liked:
+```json
+{
+    "success": {
+        "total": 1
+    },
+    "contents": {
+        "translated": "yo yo bada pik prompo",
+        "text": "yo yo check it now",
+        "translation": "minion"
+    }
+}%  
+```
+* Yes, we love Minions!
+
+* We want to orchestrate these two APIs in one request, furthermore, we will take NBA API's response's fifth player's last name and combined it into a sentence for fun translater API's to translate into `minion` language.
+
+* In Easegress, a pipeline usually represents a particular HTTP service(maybe with several backends), APIAggregator can forward the request to a dedicated pipeline. And we can use HTTPTextTemplate syntax to extract the responses and turn them into the input for the next pipeline with Aggregator.
+
+### Step 1: Create NBA API pipeline 
+
+``` bash  
+echo '
+name: pipeline-nba
+kind: HTTPPipeline
+flow:
+  - filter: requestAdp
+  - filter: proxy
+filters:
+  - kind: RequestAdaptor
+    name: requestAdp
+    host: www.balldontlie.io
+    path:
+      replace: /api/v1/players
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://www.balldontlie.io
+      loadBalance:
+        policy: roundRobin' | egctl object create 
+```
+
+### Step 2: Create Fun translator API pipeline 
+
+* This pipeline uses a `requestAdaptor` to change the request method to `POST`, replace its path to `/translate/minion.json`, and add a `Content-Type` header.
+
+``` bash
+
+echo '
+name: pipeline-translate
+kind: HTTPPipeline
+flow:
+  - filter: requestAdp
+  - filter: proxy
+filters:
+  - kind: RequestAdaptor
+    name: requestAdp
+    host: api.funtranslations.com 
+    header:
+      del: []
+      set:
+      add:
+        Content-Type: application/x-www-form-urlencoded
+    method: "POST"
+    path:
+      replace: /translate/minion.json 
+  - name: proxy
+    kind: Proxy
+    mainPool:
+      servers:
+      - url: http://api.funtranslations.com
+      loadBalance:
+        policy: roundRobin' | egctl object create 
+
+```
+
+### Step 3: Create orchestrion pipeline 
+
+* This pipeline needs to get the fifth player's last name as the input body for the translator pipeline. It achieves this goal by using `requestAdaptor` and the build-in `HTTPTemplate` in the pipeline.
+* At last, this pipeline combines the output from NBA API and Fun translator API to form a new response.
+
+``` bash 
+echo '
+name: pipeline-agg
+kind: HTTPPipeline
+flow:
+  - filter: agg-demo
+  - filter: req-adaptor1
+  - filter: agg-demo1
+  - filter: rsp-adaptor
+
+filters:
+  - pipelines:
+      - name: pipeline-nba
+    kind: APIAggregator
+    mergeResponse: true
+    name: agg-demo
+  - name: req-adaptor1
+    kind: RequestAdaptor
+    header:
+      del: [] 
+      set: 
+      add: 
+        Content-Type: application/x-www-form-urlencoded
+    body: "text=hi my name is [[filter.agg-demo.rsp.body.data.4.last_name]] yoyo check it now" 
+  - pipelines:
+      - name: pipeline-translate
+    kind: APIAggregator
+    name: agg-demo1
+    mergeResponse: true
+  - name: rsp-adaptor
+    kind: ResponseAdaptor
+    header:
+      del: [] 
+      set: 
+        last_name: "[[filter.agg-demo.rsp.body.data.4.last_name]]" 
+      add: 
+    body: "{\"name\": \"[[filter.agg-demo.rsp.body.data.4.last_name]]\",\"translated\":\"[[filter.agg-demo1.rsp.body.contents.translated]]\", \"origin\":\"[[filter.agg-demo1.rsp.body.contents.text]]\", \"language\":\"[[filter.agg-demo1.rsp.body.contents.translation]]\"}" ' | egctl  object create 
+
+```
+
+### Step 4: Create HTTPServer for routing
+
+``` bash
+echo '
+kind: HTTPServer
+name: server-demo
+certs:
+keys:
+port: 10080
+keepAlive: true
+https: false
+rules:
+  - paths:
+    - pathPrefix: /workflow
+      backend: pipeline-agg ' | egctl  object create 
+
+```
+
+### Step 5: See the result 
+
+``` bash
+$ curl http://127.0.0.1:10080/workflow -vv
+*   Trying 127.0.0.1...
+* TCP_NODELAY set
+* Connected to 127.0.0.1 (127.0.0.1) port 10080 (#0)
+> GET /workflow HTTP/1.1
+> Host: 127.0.0.1:10080
+> User-Agent: curl/7.64.1
+> Accept: */*
+> 
+< HTTP/1.1 200 OK
+< Last_name: Brown
+< Date: Wed, 04 Aug 2021 09:28:07 GMT
+< Content-Length: 144
+< Content-Type: text/plain; charset=utf-8
+< 
+{"name": "Brown","translated":"hi mi nomba tis nub yoyo bada pik prompo", "origin":"hi my name is Brown yoyo check it now", "language":"minion"}
+
+```
+
+
+* `filter.agg-demo.rsp.body.data.4.last_name` in rsp-adaptor will extract fifth player's last name fron NBA API's response body.
+* `filter.agg-demo1.rsp.body.contents.translated` in rsp-adaptor will extract the translated result in minion language.
+* The template syntax above supports GJSON[3] in the last field.
+
+## References
+
+1. https://en.wikipedia.org/wiki/Workflow
+2. https://learn.vonage.com/blog/2021/03/15/the-ultimate-list-of-fun-apis-for-your-next-coding-project/ 
+3. https://github.com/tidwall/gjson
diff --git a/doc/filters.md b/doc/filters.md
index d7c48dfb..03267f32 100644
--- a/doc/filters.md
+++ b/doc/filters.md
@@ -831,7 +831,7 @@ The relationship between `methods` and `url` is `AND`.
 
 | Name                 | Type    | Description                                                                                                                                                                                                                                                      | Required |
 | -------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
-| name                 | string  | Name of the policy. Must be unique in one CircuitBreaker configuration                                                                                                                                                                                           | Yes      |
+| name                 | string  | Name of the policy. Must be unique in one Retryer configuration                                                                                                                                                                                           | Yes      |
 | countingNetworkError | bool    | Counting network error as failure or not. Default is false                                                                                                                                                                                                       | No       |
 | failureStatusCodes   | []int   | HTTP status codes which need to be counting as failures                                                                                                                                                                                                          | No       |
 | maxAttempts          | int     | The maximum number of attempts (including the initial one). Default is 3                                                                                                                                                                                         | No       |
diff --git a/go.mod b/go.mod
index ca1512cf..99e5a1ff 100644
--- a/go.mod
+++ b/go.mod
@@ -11,8 +11,7 @@ require (
 	github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
 	github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
 	github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
-	github.com/fatih/color v1.12.0
-	github.com/fatih/structs v1.1.0
+	github.com/fatih/color v1.9.0
 	github.com/ghodss/yaml v1.0.0
 	github.com/go-chi/chi/v5 v5.0.3
 	github.com/go-zookeeper/zk v1.0.2
diff --git a/go.sum b/go.sum
index f77ebb55..a10c455d 100644
--- a/go.sum
+++ b/go.sum
@@ -432,11 +432,8 @@ github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpm
 github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
 github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
-github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
-github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
-github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
-github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
@@ -885,9 +882,8 @@ github.com/marten-seemann/qtls-go1-17 v0.1.0-beta.1.2 h1:SficYjyOthSrliKI+EaFuXS
 github.com/marten-seemann/qtls-go1-17 v0.1.0-beta.1.2/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
 github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
-github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go
index fc5c2e79..3bc0f49d 100644
--- a/pkg/cluster/cluster.go
+++ b/pkg/cluster/cluster.go
@@ -31,7 +31,6 @@ import (
 	"go.etcd.io/etcd/server/v3/embed"
 	yaml "gopkg.in/yaml.v2"
 
-	"github.com/megaease/easegress/pkg/common"
 	"github.com/megaease/easegress/pkg/logger"
 	"github.com/megaease/easegress/pkg/option"
 )
@@ -366,18 +365,6 @@ func (c *cluster) checkClusterName() error {
 	return nil
 }
 
-// removeAndBackupEtcdData is DEPRECATED,
-// will be deleted when it's sure that we won't need it.
-func (c *cluster) removeAndBackupEtcdData() {
-	if !common.IsDirEmpty(c.opt.AbsDataDir) {
-		logger.Infof("backup and clean %s", c.opt.AbsDataDir)
-		err := common.BackupAndCleanDir(c.opt.AbsDataDir)
-		if err != nil {
-			logger.Errorf("backup and clean %s failed: %v", c.opt.AbsDataDir, err)
-		}
-	}
-}
-
 func (c *cluster) getClient() (*clientv3.Client, error) {
 	c.clientMutex.RLock()
 	if c.client != nil {
@@ -622,14 +609,18 @@ func closeEtcdServer(s *embed.Etcd) {
 		<-s.Server.StopNotify()
 	default:
 		s.Server.HardStop()
+		for _, client := range s.Clients {
+			if client != nil {
+				client.Close()
+			}
+		}
+		for _, peer := range s.Peers {
+			if peer != nil {
+				peer.Close()
+			}
+		}
 		logger.Infof("hard stop server")
 	}
-	for _, client := range s.Clients {
-		client.Close()
-	}
-	for _, peer := range s.Peers {
-		peer.Close()
-	}
 }
 
 func (c *cluster) startServer() (done, timeout chan struct{}, err error) {
diff --git a/pkg/cluster/member.go b/pkg/cluster/member.go
index 6a24e3d3..bf5de6f7 100644
--- a/pkg/cluster/member.go
+++ b/pkg/cluster/member.go
@@ -197,19 +197,6 @@ func (m *members) _selfWithoutID() *member {
 	return s
 }
 
-func (m *members) isSelfIDChanged() bool {
-	return m.selfIDChanged
-}
-
-func (m *members) clusterMember() *membersSlice {
-	m.RLock()
-	defer m.RUnlock()
-
-	copied := m.ClusterMembers.copy()
-
-	return &copied
-}
-
 func (m *members) clusterMembersLen() int {
 	m.RLock()
 	defer m.RUnlock()
@@ -246,18 +233,6 @@ func (m *members) knownMembersLen() int {
 	return m.KnownMembers.Len()
 }
 
-// NOTE: Maybe use it in future in case of connecting
-// one member purged but running at another etcd cluster.
-func (m *members) deleteKnownMember(name string) {
-	m.Lock()
-	defer m.Unlock()
-
-	// NOTE: It's fine to delete myself,
-	// because it will restore myself in newMembers while restarting.
-	m.KnownMembers.deleteByName(name)
-	m.store()
-}
-
 func (m *members) knownPeerURLs() []string {
 	m.RLock()
 	defer m.RUnlock()
diff --git a/pkg/common/callback.go b/pkg/common/callback.go
deleted file mode 100644
index d09116ff..00000000
--- a/pkg/common/callback.go
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-const (
-	// CallbacksInitCapacity is the capacity of callback functions.
-	CallbacksInitCapacity = 20
-
-	// NormalPriorityCallback is the name of normal priority callback.
-	NormalPriorityCallback = "__NoRmAl_PrIoRiTy_CaLlBaCk"
-	// CriticalPriorityCallback is the name of critical priority callback.
-	CriticalPriorityCallback = "__CrItIcAl_PrIoRiTy_CaLlBaCk"
-)
-
-// NamedCallback is the named callback struct
-type NamedCallback struct {
-	name     string
-	callback interface{}
-}
-
-// NewNamedCallback returns a new named callback.
-func NewNamedCallback(name string, callback interface{}) *NamedCallback {
-	return &NamedCallback{
-		name:     name,
-		callback: callback,
-	}
-}
-
-// Name returns callback name
-func (cb *NamedCallback) Name() string {
-	return cb.name
-}
-
-// Callback returns callback function
-func (cb *NamedCallback) Callback() interface{} {
-	return cb.callback
-}
-
-// SetCallback sets the callback function
-func (cb *NamedCallback) SetCallback(callback interface{}) interface{} {
-	oriCallback := cb.callback
-	cb.callback = callback
-	return oriCallback
-}
-
-type namedCallbackWithIdx struct {
-	cb         *NamedCallback
-	idxOfOrder int
-}
-
-// NamedCallbackSet is a set of NamedCallback
-type NamedCallbackSet struct {
-	// critical callback takes low index, normal callback takes high index
-	callbacks []*NamedCallback                 // index for access by order
-	names     map[string]*namedCallbackWithIdx // index for access by callback constant
-
-}
-
-// NewNamedCallbackSet returns a new NamedCallbackSet
-func NewNamedCallbackSet() *NamedCallbackSet {
-	return &NamedCallbackSet{
-		names:     make(map[string]*namedCallbackWithIdx, CallbacksInitCapacity),
-		callbacks: make([]*NamedCallback, 0, CallbacksInitCapacity),
-	}
-}
-
-// CopyCallbacks copies the current callbacks
-func (cbs *NamedCallbackSet) CopyCallbacks() []*NamedCallback {
-	ret := make([]*NamedCallback, len(cbs.callbacks))
-	copy(ret, cbs.callbacks)
-	return ret
-}
-
-// GetCallbacks get callbacks
-func (cbs *NamedCallbackSet) GetCallbacks() []*NamedCallback {
-	return cbs.callbacks
-}
-
-////
-
-// AddCallback adds a callback into a callback set with priority
-func AddCallback(cbs *NamedCallbackSet, name string, callback interface{}, priority string) *NamedCallbackSet {
-	if cbs == nil {
-		return nil
-	}
-
-	_, exists := cbs.names[name]
-	if exists {
-		return cbs
-	}
-
-	cb := NewNamedCallback(name, callback)
-	idx := 0
-
-	if priority == NormalPriorityCallback {
-		idx = len(cbs.callbacks)
-		cbs.callbacks = append(cbs.callbacks, cb)
-	} else if priority == CriticalPriorityCallback {
-		// idx = 0
-		cbs.callbacks = append([]*NamedCallback{cb}, cbs.callbacks...)
-	} else {
-		idx = len(cbs.callbacks)
-		for i, namedCallback := range cbs.callbacks {
-			if namedCallback.Name() == priority {
-				idx = i
-				break
-			}
-		}
-
-		// insert before the pos
-		cbs.callbacks = append(cbs.callbacks[:idx], append([]*NamedCallback{cb}, cbs.callbacks[idx:]...)...)
-	}
-
-	cbs.names[name] = &namedCallbackWithIdx{
-		cb:         cb,
-		idxOfOrder: idx,
-	}
-
-	return cbs
-}
-
-// DeleteCallback deletes a callback in callback set
-func DeleteCallback(cbs *NamedCallbackSet, name string) *NamedCallbackSet {
-	if cbs == nil {
-		return nil
-	}
-
-	cbi, exists := cbs.names[name]
-	if !exists {
-		return cbs
-	}
-
-	delete(cbs.names, name)
-	cbs.callbacks = append(cbs.callbacks[:cbi.idxOfOrder], cbs.callbacks[cbi.idxOfOrder+1:]...)
-
-	for _, cbi1 := range cbs.names {
-		if cbi1.idxOfOrder > cbi.idxOfOrder {
-			cbi1.idxOfOrder--
-		}
-	}
-
-	return cbs
-}
diff --git a/pkg/common/lazybuf.go b/pkg/common/lazybuf.go
deleted file mode 100644
index f3eb64f0..00000000
--- a/pkg/common/lazybuf.go
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-// NOTE: Copy from path/path.go in standard library.
-
-// A Lazybuf is a lazily constructed path buffer.
-// It supports append, reading previously appended bytes,
-// and retrieving the final string. It does not allocate a buffer
-// to hold the output until that output diverges from s.
-type Lazybuf struct {
-	s   string
-	buf []byte
-	w   int
-}
-
-// NewLazybuf returns lazy buffer
-func NewLazybuf(s string) *Lazybuf {
-	return &Lazybuf{
-		s: s,
-	}
-}
-
-// Index returns a byte by its index
-func (b *Lazybuf) Index(i int) byte {
-	if b.buf != nil {
-		return b.buf[i]
-	}
-	return b.s[i]
-}
-
-// Append appends a byte into buffer
-func (b *Lazybuf) Append(c byte) {
-	if b.buf == nil {
-		if b.w < len(b.s) && b.s[b.w] == c {
-			b.w++
-			return
-		}
-		b.buf = make([]byte, len(b.s))
-		copy(b.buf, b.s[:b.w])
-	}
-	b.buf[b.w] = c
-	b.w++
-}
-
-// String returns the string of lazybuf
-func (b *Lazybuf) String() string {
-	if b.buf == nil {
-		return b.s[:b.w]
-	}
-	return string(b.buf[:b.w])
-}
diff --git a/pkg/common/sample.go b/pkg/common/sample.go
deleted file mode 100644
index 51cd9ed9..00000000
--- a/pkg/common/sample.go
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-import (
-	"math"
-	"sync"
-	"time"
-
-	metrics "github.com/rcrowley/go-metrics"
-)
-
-// ExpDecaySample is the structure use for monitoring metrics(such as: P90, P50 etc)
-type ExpDecaySample struct {
-	bucketIdxLock sync.RWMutex
-	bucketIdx     uint64
-	buckets       []metrics.Sample
-	stop          chan struct{}
-	closeLock     sync.RWMutex
-	closed        bool
-}
-
-// NewExpDecaySample creates ExpDecaySample structure
-func NewExpDecaySample(timeRange time.Duration, secondsForEachBucket int) *ExpDecaySample {
-	if secondsForEachBucket == 0 {
-		secondsForEachBucket = int(timeRange / 2)
-	}
-
-	s := new(ExpDecaySample)
-
-	n := uint64(math.Ceil(timeRange.Seconds()/float64(secondsForEachBucket))) + 1
-
-	var idx uint64
-	for idx < n {
-		s.buckets = append(s.buckets, metrics.NewExpDecaySample(514, 0.015))
-		idx++
-	}
-
-	s.stop = make(chan struct{})
-	switcher := func() {
-		ticker := time.NewTicker(time.Second * time.Duration(secondsForEachBucket))
-		defer ticker.Stop()
-		for {
-			select {
-			case <-ticker.C:
-				s.bucketIdxLock.Lock()
-
-				s.buckets[s.bucketIdx].Clear()
-				s.bucketIdx = (s.bucketIdx + 1) % n
-
-				s.bucketIdxLock.Unlock()
-			case <-s.stop:
-				return
-			}
-		}
-	}
-
-	go switcher()
-
-	return s
-}
-
-// Close cleans the ExpDecaySample.
-func (s *ExpDecaySample) Close() {
-	s.closeLock.Lock()
-	defer s.closeLock.Unlock()
-
-	if s.closed {
-		return
-	}
-
-	close(s.stop)
-	s.closed = true
-
-	// defense, accelerate gc
-	for i := 0; i < len(s.buckets); i++ {
-		s.buckets[i].Clear()
-	}
-}
-
-// Update updates the ExpDecaySample
-func (s *ExpDecaySample) Update(v int64) {
-	for i := 0; i < len(s.buckets); i++ {
-		s.buckets[i].Update(v)
-	}
-}
-
-// Percentile calculates the percentile
-func (s *ExpDecaySample) Percentile(q float64) float64 {
-	s.bucketIdxLock.RLock()
-	defer s.bucketIdxLock.RUnlock()
-
-	idx := (s.bucketIdx + 1) % uint64(len(s.buckets))
-
-	return s.buckets[idx].Percentile(q)
-}
-
-// StdDev return the standard deviation value
-func (s *ExpDecaySample) StdDev() float64 {
-	s.bucketIdxLock.RLock()
-	defer s.bucketIdxLock.RUnlock()
-
-	idx := (s.bucketIdx + 1) % uint64(len(s.buckets))
-
-	return s.buckets[idx].StdDev()
-}
-
-// Max returns the max value
-func (s *ExpDecaySample) Max() int64 {
-	s.bucketIdxLock.RLock()
-	defer s.bucketIdxLock.RUnlock()
-
-	idx := (s.bucketIdx + 1) % uint64(len(s.buckets))
-
-	return s.buckets[idx].Max()
-}
-
-// Min returns the min value
-func (s *ExpDecaySample) Min() int64 {
-	s.bucketIdxLock.RLock()
-	defer s.bucketIdxLock.RUnlock()
-
-	idx := (s.bucketIdx + 1) % uint64(len(s.buckets))
-
-	return s.buckets[idx].Min()
-}
-
-// Count returns the count value
-func (s *ExpDecaySample) Count() int64 {
-	s.bucketIdxLock.RLock()
-	defer s.bucketIdxLock.RUnlock()
-
-	idx := (s.bucketIdx + 1) % uint64(len(s.buckets))
-
-	return s.buckets[idx].Count()
-}
-
-// Variance returns the variance value
-func (s *ExpDecaySample) Variance() float64 {
-	s.bucketIdxLock.RLock()
-	defer s.bucketIdxLock.RUnlock()
-
-	idx := (s.bucketIdx + 1) % uint64(len(s.buckets))
-
-	return s.buckets[idx].Variance()
-}
-
-// Sum returns the sum value
-func (s *ExpDecaySample) Sum() int64 {
-	s.bucketIdxLock.RLock()
-	defer s.bucketIdxLock.RUnlock()
-
-	idx := (s.bucketIdx + 1) % uint64(len(s.buckets))
-
-	return s.buckets[idx].Sum()
-}
diff --git a/pkg/common/stat_aggregator.go b/pkg/common/stat_aggregator.go
deleted file mode 100644
index c79bfaa8..00000000
--- a/pkg/common/stat_aggregator.go
+++ /dev/null
@@ -1,277 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-import (
-	"fmt"
-	"reflect"
-)
-
-// StatAggregator is the interface
-type StatAggregator interface {
-	Aggregate(interface{}) error
-	Result() interface{}
-	String() string
-}
-
-type numericKind uint
-
-const (
-	invalidNum numericKind = iota
-	intNum
-	uintNum
-	floatNum
-)
-
-// String returns the type of number.
-func (nk numericKind) String() string {
-	switch nk {
-	case invalidNum:
-		return "invalid"
-	case intNum:
-		return "int[8|16|32|64]"
-	case uintNum:
-		return "uint[8|16|32|64]"
-	case floatNum:
-		return "float[32|64]"
-	}
-	return "unknown numericKind"
-}
-
-func unifyNumericKind(num interface{}) (interface{}, numericKind) {
-	switch reflect.ValueOf(num).Kind() {
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		return reflect.ValueOf(num).Int(), intNum
-	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-		return reflect.ValueOf(num).Uint(), uintNum
-	case reflect.Float32, reflect.Float64:
-		return reflect.ValueOf(num).Float(), floatNum
-	default:
-		return num, invalidNum
-	}
-}
-
-// NumericMaxAggregator is the structure with max value
-type NumericMaxAggregator struct {
-	max interface{}
-	nk  numericKind
-}
-
-// String returns the name.
-func (a *NumericMaxAggregator) String() string {
-	return "numeric_max"
-}
-
-// Aggregate records the max of value
-func (a *NumericMaxAggregator) Aggregate(num interface{}) error {
-	if num == nil {
-		return nil
-	}
-
-	kind := reflect.ValueOf(num).Kind()
-	value, nk := unifyNumericKind(num)
-	if nk == invalidNum {
-		return fmt.Errorf("unsupported kind %s", kind)
-	}
-
-	if a.max == nil {
-		a.max, a.nk = value, nk
-		return nil
-	}
-
-	if a.nk != nk {
-		return fmt.Errorf("want kind %s, got %s", a.nk, kind)
-	}
-
-	switch a.nk {
-	case intNum:
-		if a.max.(int64) < value.(int64) {
-			a.max = value
-		}
-	case uintNum:
-		if a.max.(uint64) < value.(uint64) {
-			a.max = value
-		}
-	case floatNum:
-		if a.max.(float64) < value.(float64) {
-			a.max = value
-		}
-	default:
-		return fmt.Errorf("BUG: unsupported numeric kind %s", a.nk)
-	}
-
-	return nil
-}
-
-// Result return the max value
-func (a *NumericMaxAggregator) Result() interface{} {
-	return a.max
-}
-
-// NumericMinAggregator is the structure with min value
-type NumericMinAggregator struct {
-	min interface{}
-	nk  numericKind
-}
-
-// String returns the name.
-func (a *NumericMinAggregator) String() string {
-	return "numeric_min"
-}
-
-// Aggregate records the min value
-func (a *NumericMinAggregator) Aggregate(num interface{}) error {
-	if num == nil {
-		return nil
-	}
-
-	kind := reflect.ValueOf(num).Kind()
-	value, nk := unifyNumericKind(num)
-	if nk == invalidNum {
-		return fmt.Errorf("unsupported kind %s", kind)
-	}
-
-	if a.min == nil {
-		a.min, a.nk = value, nk
-		return nil
-	}
-
-	if a.nk != nk {
-		return fmt.Errorf("want kind %s, got %s", a.nk, kind)
-	}
-
-	switch a.nk {
-	case intNum:
-		if a.min.(int64) > value.(int64) {
-			a.min = value
-		}
-	case uintNum:
-		if a.min.(uint64) > value.(uint64) {
-			a.min = value
-		}
-	case floatNum:
-		if a.min.(float64) > value.(float64) {
-			a.min = value
-		}
-	default:
-		return fmt.Errorf("BUG: unsupported numeric kind %s", a.nk)
-	}
-
-	return nil
-}
-
-// Result returns the min value.
-func (a *NumericMinAggregator) Result() interface{} {
-	return a.min
-}
-
-// NumericSumAggregator is the structure with sum value
-type NumericSumAggregator struct {
-	sum interface{}
-	nk  numericKind
-}
-
-// String returns the name.
-func (a *NumericSumAggregator) String() string {
-	return "numeric_sum"
-}
-
-// Aggregate records the sum value
-func (a *NumericSumAggregator) Aggregate(num interface{}) error {
-	if num == nil {
-		return nil
-	}
-
-	kind := reflect.ValueOf(num).Kind()
-	value, nk := unifyNumericKind(num)
-	if nk == invalidNum {
-		return fmt.Errorf("unsupported kind %s", kind)
-	}
-
-	if a.sum == nil {
-		a.sum, a.nk = value, nk
-		return nil
-	}
-
-	if a.nk != nk {
-		return fmt.Errorf("want kind %s, got %s", a.nk, kind)
-	}
-
-	switch a.nk {
-	case intNum:
-		a.sum = a.sum.(int64) + value.(int64)
-	case uintNum:
-		a.sum = a.sum.(uint64) + value.(uint64)
-	case floatNum:
-		a.sum = a.sum.(float64) + value.(float64)
-	default:
-		return fmt.Errorf("BUG: unsupported numeric kind %s", a.nk)
-	}
-
-	return nil
-}
-
-// Result returns the sum value.
-func (a *NumericSumAggregator) Result() interface{} {
-	return a.sum
-}
-
-// NumericAvgAggregator is the structure with average value
-type NumericAvgAggregator struct {
-	NumericSumAggregator
-	count int64
-}
-
-// String returns the name.
-func (a *NumericAvgAggregator) String() string {
-	return "numeric_average"
-}
-
-// Aggregate records the number of values and the sum of values
-func (a *NumericAvgAggregator) Aggregate(num interface{}) error {
-	if num == nil {
-		return nil
-	}
-
-	err := a.NumericSumAggregator.Aggregate(num)
-	if err != nil {
-		return err
-	}
-
-	a.count++
-
-	return nil
-}
-
-// Result returns the average values
-func (a *NumericAvgAggregator) Result() interface{} {
-	if a.NumericSumAggregator.Result() == nil {
-		return nil
-	}
-
-	switch a.nk {
-	case intNum:
-		return a.NumericSumAggregator.Result().(int64) / int64(a.count)
-	case uintNum:
-		return a.NumericSumAggregator.Result().(uint64) / uint64(a.count)
-	case floatNum:
-		return a.NumericSumAggregator.Result().(float64) / float64(a.count)
-	default:
-		return nil
-	}
-}
diff --git a/pkg/common/strings.go b/pkg/common/strings.go
deleted file mode 100644
index f5f2fa8b..00000000
--- a/pkg/common/strings.go
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-import (
-	"reflect"
-	"unsafe"
-)
-
-// B2s converts byte slice to a string without memory allocation.
-// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
-//
-// Note it may break if string and/or slice header will change
-// in the future go versions.
-func B2s(b []byte) string {
-	return *(*string)(unsafe.Pointer(&b))
-}
-
-// S2b converts string to a byte slice without memory allocation.
-//
-// Note it may break if string and/or slice header will change
-// in the future go versions.
-func S2b(s string) []byte {
-	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
-	bh := reflect.SliceHeader{
-		Data: sh.Data,
-		Len:  sh.Len,
-		Cap:  sh.Len,
-	}
-	return *(*[]byte)(unsafe.Pointer(&bh))
-}
diff --git a/pkg/common/sys_proc_linux.go b/pkg/common/sys_proc_linux.go
deleted file mode 100644
index e5691e3f..00000000
--- a/pkg/common/sys_proc_linux.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// +build linux
-
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-import (
-	"syscall"
-)
-
-// SysProcAttr returns system process attribute
-func SysProcAttr() *syscall.SysProcAttr {
-	return &syscall.SysProcAttr{
-		Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWIPC |
-			syscall.CLONE_NEWPID | syscall.CLONE_NEWUTS | syscall.CLONE_NEWUSER,
-	}
-}
diff --git a/pkg/common/sys_proc_others.go b/pkg/common/sys_proc_others.go
deleted file mode 100644
index 084c47b8..00000000
--- a/pkg/common/sys_proc_others.go
+++ /dev/null
@@ -1,29 +0,0 @@
-// +build darwin dragonfly freebsd netbsd openbsd solaris windows plan9
-
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-import (
-	"syscall"
-)
-
-// SysProcAttr returns system process attribute
-func SysProcAttr() *syscall.SysProcAttr {
-	return nil
-}
diff --git a/pkg/common/utils.go b/pkg/common/utils.go
index ceffcaed..a8d78b12 100644
--- a/pkg/common/utils.go
+++ b/pkg/common/utils.go
@@ -18,209 +18,14 @@
 package common
 
 import (
-	"bytes"
 	"fmt"
 	"io"
 	"os"
 	"path/filepath"
 	"regexp"
 	"runtime"
-	"strconv"
-	"strings"
 )
 
-// TokenEscapeChar is the escape char
-const TokenEscapeChar string = `\`
-
-// GraphiteSplit slices s into all substrings separated by sep
-// returns a slice of the substrings without length prefix separated by lensep.
-// The routine does its best to split without error returned.
-func GraphiteSplit(s string, lensep string, sep string) []string {
-	a := make([]string, 0)
-
-	i := 0
-	n := len(s)
-	for i < n {
-		j := strings.Index(s[i:], lensep)
-		if j == -1 {
-			return a
-		}
-		j += i
-
-		length, err := strconv.ParseUint(s[i:j], 10, 16)
-		if err != nil {
-			return a
-		}
-
-		i = j + len(lensep)
-		j = i + int(length)
-		if j > n {
-			return a
-		}
-
-		a = append(a, s[i:j])
-
-		i = j
-		if i < n && s[i:i+len(sep)] != sep {
-			return a
-		}
-
-		i += len(sep)
-	}
-	return a
-}
-
-// TokenVisitor uses for visit token
-type TokenVisitor func(pos int, token string) (care bool, replacement string)
-
-// ScanTokens scans tokens.
-func ScanTokens(str string, removeEscapeChar bool, visitor TokenVisitor) (string, error) {
-	if visitor == nil {
-		visitor = func(_ int, _ string) (bool, string) {
-			return false, ""
-		}
-	}
-
-	in := false
-	ret := bytes.NewBuffer(nil)
-	v := []byte(str)
-
-	escaped := func(pos int) bool {
-		if pos == 0 {
-			return false
-		}
-
-		return string(v[pos-1]) == TokenEscapeChar
-	}
-
-	escaper := func(s string) string {
-		return strings.Replace(
-			strings.Replace(s, TokenEscapeChar+`{`, "{", -1),
-			TokenEscapeChar+`}`, "}", -1)
-	}
-
-	token := bytes.NewBuffer(nil)
-	for i, c := range v {
-		if c == '{' && !escaped(i) {
-			if in {
-				return str, fmt.Errorf("invalid pattern string")
-			}
-
-			in = true
-		} else if c == '}' && !escaped(i) {
-			if !in {
-				return str, fmt.Errorf("invalid pattern string")
-			}
-
-			if len(strings.TrimSpace(token.String())) == 0 {
-				return str, fmt.Errorf("empty token")
-			}
-
-			pos := i - token.Len() - 1
-			token = bytes.NewBufferString(escaper(token.String()))
-
-			care, replacement := visitor(pos, token.String())
-			if care {
-				ret.WriteString(replacement)
-				c = 0
-			} else {
-				ret.WriteString("{")
-				ret.WriteString(token.String())
-			}
-
-			token = bytes.NewBuffer(nil)
-			in = false
-		} else if in {
-			token.WriteByte(c)
-		}
-
-		if !in && c != 0 {
-			ret.WriteByte(c)
-		}
-	}
-
-	if in {
-		return str, fmt.Errorf("invalid pattern string")
-	}
-
-	retStr := ret.String()
-	if removeEscapeChar {
-		retStr = escaper(retStr)
-	}
-
-	return retStr, nil
-}
-
-// PanicToErr try to recover panic and returns if it happened or not.
-func PanicToErr(f func(), err *error) (failed bool) {
-	defer func() {
-		x := recover()
-		if x == nil {
-			failed = false
-			return
-		}
-
-		failed = true
-
-		if err == nil {
-			return
-		}
-
-		switch e := x.(type) {
-		case error:
-			*err = e
-		case string:
-			*err = fmt.Errorf(e)
-		default:
-			*err = fmt.Errorf("%v", x)
-		}
-	}()
-
-	f()
-
-	return
-}
-
-var (
-	// TrueStrings contains all string represents true
-	TrueStrings = []string{"1", "t", "true", "on", "y", "yes"}
-	// FalseStrings contains all string represents false
-	FalseStrings = []string{"0", "f", "false", "off", "n", "no"}
-)
-
-// RemoveRepeatedByte removes the repeated bytes.
-func RemoveRepeatedByte(s string, needRemoveByte byte) string {
-	if len(s) < 2 {
-		return s
-	}
-
-	out := NewLazybuf(s)
-	repeatingByte := s[0]
-	out.Append(repeatingByte)
-	for _, c := range []byte(s[1:]) {
-		if c != repeatingByte || c != needRemoveByte {
-			out.Append(c)
-		}
-		repeatingByte = c
-	}
-	return out.String()
-}
-
-// NextNumberPowerOf2 return the number of power of 2
-// Via: https://stackoverflow.com/a/466242/1705845
-//      https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
-func NextNumberPowerOf2(v uint64) uint64 {
-	v--
-	v |= v >> 1
-	v |= v >> 2
-	v |= v >> 4
-	v |= v >> 8
-	v |= v >> 16
-	v |= v >> 32
-	v++
-	return v
-}
-
 // URLFriendlyCharactersRegex - safe characters for friendly url, rfc3986 section 2.3
 var URLFriendlyCharactersRegex = regexp.MustCompile(`^[A-Za-z0-9\-_\.~]{1,253}$`)
 
diff --git a/pkg/common/uuid.go b/pkg/common/uuid.go
deleted file mode 100644
index 49be20c0..00000000
--- a/pkg/common/uuid.go
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-import (
-	"crypto/rand"
-	"fmt"
-	"io"
-)
-
-// UUID returns an UUID
-func UUID() (string, error) {
-	uuid := make([]byte, 16)
-	n, err := io.ReadFull(rand.Reader, uuid)
-	if n != len(uuid) || err != nil {
-		return "", err
-	}
-
-	uuid[8] = uuid[8]&^0xc0 | 0x80
-	uuid[6] = uuid[6]&^0xf0 | 0x40
-
-	return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
-}
diff --git a/pkg/filter/circuitbreaker/circuitbreaker.go b/pkg/filter/circuitbreaker/circuitbreaker.go
index 5543bc86..40699717 100644
--- a/pkg/filter/circuitbreaker/circuitbreaker.go
+++ b/pkg/filter/circuitbreaker/circuitbreaker.go
@@ -107,7 +107,7 @@ URLLoop:
 	return nil
 }
 
-func (url *URLRule) createCircuitBreaker() {
+func (url *URLRule) buildPolicy() *libcb.Policy {
 	policy := libcb.Policy{
 		FailureRateThreshold:             url.policy.FailureRateThreshold,
 		SlowCallRateThreshold:            url.policy.SlowCallRateThreshold,
@@ -157,7 +157,12 @@ func (url *URLRule) createCircuitBreaker() {
 		policy.WaitDurationInOpen = time.Minute
 	}
 
-	url.cb = libcb.New(&policy)
+	return &policy
+}
+
+func (url *URLRule) createCircuitBreaker() {
+	policy := url.buildPolicy()
+	url.cb = libcb.New(policy)
 }
 
 // Kind returns the kind of CircuitBreaker.
diff --git a/pkg/filter/mock/mock.go b/pkg/filter/mock/mock.go
index 6d2130c6..781e4c2a 100644
--- a/pkg/filter/mock/mock.go
+++ b/pkg/filter/mock/mock.go
@@ -103,11 +103,7 @@ func (m *Mock) reload() {
 		if r.Delay == "" {
 			continue
 		}
-		var err error
-		r.delay, err = time.ParseDuration(r.Delay)
-		if err != nil {
-			logger.Errorf("BUG: parse duration %s failed: %v", r.Delay, err)
-		}
+		r.delay, _ = time.ParseDuration(r.Delay)
 	}
 }
 
@@ -129,7 +125,7 @@ func (m *Mock) handle(ctx context.HTTPContext) (result string) {
 		w.SetBody(strings.NewReader(rule.Body))
 		result = resultMocked
 
-		if rule.delay < 0 {
+		if rule.delay <= 0 {
 			return
 		}
 
@@ -147,7 +143,12 @@ func (m *Mock) handle(ctx context.HTTPContext) (result string) {
 			return
 		}
 
-		if rule.Path == path || strings.HasPrefix(path, rule.PathPrefix) {
+		if rule.Path == path {
+			mock(rule)
+			return
+		}
+
+		if rule.PathPrefix != "" && strings.HasPrefix(path, rule.PathPrefix) {
 			mock(rule)
 			return
 		}
diff --git a/pkg/filter/proxy/compression.go b/pkg/filter/proxy/compression.go
index dbd9c8d6..0a33f6b4 100644
--- a/pkg/filter/proxy/compression.go
+++ b/pkg/filter/proxy/compression.go
@@ -54,7 +54,7 @@ type (
 	}
 )
 
-func newcompression(spec *CompressionSpec) *compression {
+func newCompression(spec *CompressionSpec) *compression {
 	return &compression{
 		spec: spec,
 	}
diff --git a/pkg/filter/proxy/pool.go b/pkg/filter/proxy/pool.go
index ce8f5dfc..1df07425 100644
--- a/pkg/filter/proxy/pool.go
+++ b/pkg/filter/proxy/pool.go
@@ -212,7 +212,7 @@ func (p *pool) doRequest(ctx context.HTTPContext, req *request) (*http.Response,
 	span := ctx.Span().NewChildWithStart(spanName, req.startTime())
 	span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.std.Header))
 
-	resp, err := globalClient.Do(req.std)
+	resp, err := fnSendRequest(req.std)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/filter/proxy/proxy.go b/pkg/filter/proxy/proxy.go
index f083bdf9..f2af9cac 100644
--- a/pkg/filter/proxy/proxy.go
+++ b/pkg/filter/proxy/proxy.go
@@ -82,6 +82,10 @@ var globalClient = &http.Client{
 	},
 }
 
+var fnSendRequest = func(r *http.Request) (*http.Response, error) {
+	return globalClient.Do(r)
+}
+
 type (
 	// Proxy is the filter Proxy.
 	Proxy struct {
@@ -212,7 +216,7 @@ func (b *Proxy) reload() {
 	}
 
 	if b.spec.Compression != nil {
-		b.compression = newcompression(b.spec.Compression)
+		b.compression = newCompression(b.spec.Compression)
 	}
 }
 
diff --git a/pkg/filter/proxy/server.go b/pkg/filter/proxy/server.go
index fb76ef15..343f2a71 100644
--- a/pkg/filter/proxy/server.go
+++ b/pkg/filter/proxy/server.go
@@ -31,8 +31,14 @@ import (
 	"github.com/megaease/easegress/pkg/util/stringtool"
 )
 
+// make it mockable in test
+var fnGetService atomic.Value
+
 func init() {
 	rand.Seed(time.Now().UnixNano())
+	fnGetService.Store(func(serviceRegistry, serviceName string) (*serviceregistry.Service, error) {
+		return serviceregistry.Global.GetService(serviceRegistry, serviceName)
+	})
 }
 
 const (
@@ -180,11 +186,16 @@ func (s *servers) useStaticServers() {
 	s.service = nil
 }
 
+func getService(serviceRegistry, serviceName string) (*serviceregistry.Service, error) {
+	fn := fnGetService.Load().(func(serviceRegistry, serviceName string) (*serviceregistry.Service, error))
+	return fn(serviceRegistry, serviceName)
+}
+
 func (s *servers) useService() (*serviceregistry.Service, error) {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
 
-	service, err := serviceregistry.Global.GetService(s.poolSpec.ServiceRegistry, s.poolSpec.ServiceName)
+	service, err := getService(s.poolSpec.ServiceRegistry, s.poolSpec.ServiceName)
 	if err != nil {
 		return nil, fmt.Errorf("get service %s failed: %v", s.poolSpec.ServiceName, err)
 	}
diff --git a/pkg/filter/responseadaptor/responseadaptor.go b/pkg/filter/responseadaptor/responseadaptor.go
index 15ea5e96..0a1ab348 100644
--- a/pkg/filter/responseadaptor/responseadaptor.go
+++ b/pkg/filter/responseadaptor/responseadaptor.go
@@ -98,18 +98,18 @@ func (ra *ResponseAdaptor) handle(ctx context.HTTPContext) string {
 	hte := ctx.Template()
 	ctx.Response().Header().Adapt(ra.spec.Header, hte)
 
-	if len(ra.spec.Body) != 0 {
-		if hte.HasTemplates(ra.spec.Body) {
-			if body, err := hte.Render(ra.spec.Body); err != nil {
-				logger.Errorf("BUG responseadaptor render body faile , template %s , err %v",
-					ra.spec.Body, err)
-			} else {
-				ctx.Response().SetBody(bytes.NewReader([]byte(body)))
-			}
-		} else {
-			ctx.Response().SetBody(bytes.NewReader([]byte(ra.spec.Body)))
-		}
+	if len(ra.spec.Body) == 0 {
+		return ""
 	}
+
+	if !hte.HasTemplates(ra.spec.Body) {
+		ctx.Response().SetBody(bytes.NewReader([]byte(ra.spec.Body)))
+	} else if body, err := hte.Render(ra.spec.Body); err != nil {
+		logger.Errorf("BUG responseadaptor render body failed, template %s , err %v", ra.spec.Body, err)
+	} else {
+		ctx.Response().SetBody(bytes.NewReader([]byte(body)))
+	}
+
 	return ""
 }
 
diff --git a/pkg/filter/validator/oauth2.go b/pkg/filter/validator/oauth2.go
index 727d0bef..62744265 100644
--- a/pkg/filter/validator/oauth2.go
+++ b/pkg/filter/validator/oauth2.go
@@ -93,6 +93,11 @@ func NewOAuth2Validator(spec *OAuth2ValidatorSpec) *OAuth2Validator {
 	return v
 }
 
+// make it mockable
+var fnSendRequest = func(client *http.Client, r *http.Request) (*http.Response, error) {
+	return client.Do(r)
+}
+
 func (v *OAuth2Validator) introspectToken(tokenStr string) (*tokenInfo, error) {
 	var body bytes.Buffer
 	body.WriteString("token=")
@@ -111,7 +116,7 @@ func (v *OAuth2Validator) introspectToken(tokenStr string) (*tokenInfo, error) {
 		r.Header.Set("Authorization", "Basic "+v.spec.TokenIntrospect.BasicAuth)
 	}
 
-	resp, e := v.client.Do(r)
+	resp, e := fnSendRequest(v.client, r)
 	if e != nil {
 		return nil, e
 	}
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
index 3c44aaf0..9c478ce9 100644
--- a/pkg/logger/logger.go
+++ b/pkg/logger/logger.go
@@ -37,6 +37,18 @@ func Init(opt *option.Options) {
 	initRestAPI(opt)
 }
 
+// InitNop initializes all logger as nop, mainly for unit testing
+func InitNop() {
+	nop := zap.NewNop()
+	httpFilterAccessLogger = nop
+	httpFilterDumpLogger = nop
+	restAPILogger = nop
+
+	defaultLogger = nop.Sugar()
+	gressLogger = defaultLogger
+	stderrLogger = defaultLogger
+}
+
 const (
 	stdoutFilename           = "stdout.log"
 	filterHTTPAccessFilename = "filter_http_access.log"
diff --git a/pkg/object/httppipeline/httppipeline.go b/pkg/object/httppipeline/httppipeline.go
index 7fd9f6b1..115113e3 100644
--- a/pkg/object/httppipeline/httppipeline.go
+++ b/pkg/object/httppipeline/httppipeline.go
@@ -444,6 +444,18 @@ func (hp *HTTPPipeline) Handle(ctx context.HTTPContext) {
 	filterStat := &FilterStat{}
 
 	handle := func(lastResult string) string {
+		// For saving the `filterIndex`'s filter generated HTTP Response.
+		// Note: the sequence of pipeline is stack-liked, we save the filter's response into template
+		// at the beginning of the next filter.
+		if filterIndex != -1 {
+			name := hp.runningFilters[filterIndex].spec.Name()
+			if err := ctx.SaveRspToTemplate(name); err != nil {
+				format := "save http rsp failed, dict is %#v err is %v"
+				logger.Errorf(format, ctx.Template().GetDict(), err)
+			}
+			logger.Debugf("filter %s, saved response dict %v", name, ctx.Template().GetDict())
+		}
+
 		// Filters are called recursively as a stack, so we need to save current
 		// state and restore it before return
 		lastIndex := filterIndex
@@ -468,6 +480,7 @@ func (hp *HTTPPipeline) Handle(ctx context.HTTPContext) {
 			logger.Errorf(format, ctx.Template().GetDict(), err)
 		}
 
+		logger.Debugf("filter %s saved request dict %v", name, ctx.Template().GetDict())
 		filterStat = &FilterStat{Name: name, Kind: filter.spec.Kind()}
 
 		startTime := time.Now()
@@ -476,11 +489,6 @@ func (hp *HTTPPipeline) Handle(ctx context.HTTPContext) {
 		filterStat.Duration = time.Since(startTime)
 		filterStat.Result = result
 
-		if err := ctx.SaveRspToTemplate(name); err != nil {
-			format := "save http rsp failed, dict is %#v err is %v"
-			logger.Errorf(format, ctx.Template().GetDict(), err)
-		}
-
 		lastStat.Next = append(lastStat.Next, filterStat)
 		return result
 	}
diff --git a/pkg/object/trafficcontroller/trafficcontroller.go b/pkg/object/trafficcontroller/trafficcontroller.go
index f002f5ed..84af72d4 100644
--- a/pkg/object/trafficcontroller/trafficcontroller.go
+++ b/pkg/object/trafficcontroller/trafficcontroller.go
@@ -315,6 +315,9 @@ func (tc *TrafficController) GetHTTPServer(namespace, name string) (*supervisor.
 	}
 
 	entity, exists := space.httpservers.Load(name)
+	if !exists {
+		return nil, false
+	}
 
 	return entity.(*supervisor.ObjectEntity), exists
 }
@@ -546,6 +549,9 @@ func (tc *TrafficController) GetHTTPPipeline(namespace, name string) (*superviso
 	}
 
 	entity, exists := space.httppipelines.Load(name)
+	if !exists {
+		return nil, false
+	}
 
 	return entity.(*supervisor.ObjectEntity), exists
 }
diff --git a/pkg/util/httpfilter/httpfilter.go b/pkg/util/httpfilter/httpfilter.go
index c701d7e1..526d4d2f 100644
--- a/pkg/util/httpfilter/httpfilter.go
+++ b/pkg/util/httpfilter/httpfilter.go
@@ -102,10 +102,9 @@ func New(spec *Spec) *HTTPFilter {
 func (hf *HTTPFilter) Filter(ctx context.HTTPContext) bool {
 	if len(hf.spec.Headers) > 0 {
 		matchHeader := hf.filterHeader(ctx)
-		if len(hf.spec.URLs) > 0 {
-			return matchHeader && hf.filterURL(ctx)
+		if matchHeader && len(hf.spec.URLs) > 0 {
+			return hf.filterURL(ctx)
 		}
-
 		return matchHeader
 	}
 
diff --git a/pkg/util/jmxtool/agent_controller.go b/pkg/util/jmxtool/agent_controller.go
index 620cb8ef..69d2dd97 100644
--- a/pkg/util/jmxtool/agent_controller.go
+++ b/pkg/util/jmxtool/agent_controller.go
@@ -20,7 +20,6 @@ package jmxtool
 import (
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
 	"net/http"
 	"strconv"
 
@@ -75,17 +74,11 @@ func (agent *AgentClient) UpdateService(newService *spec.Service, version int64)
 	}
 
 	url := agent.URL + serviceConfigURL
-	resp, err := handleRequest(http.MethodPut, url, bytes)
+	bodyString, err := handleRequest(http.MethodPut, url, bytes)
 	if err != nil {
 		return fmt.Errorf("handleRequest error: %v", err)
 	}
-
-	bodyBytes, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return fmt.Errorf("read the http body failed: %v", err)
-	}
-	bodyString := string(bodyBytes)
-	logger.Infof("Update Service, URL: %s,request: %s, result: %v", url, string(bytes), bodyString)
+	logger.Infof("Update Service, URL: %s,request: %s, result: %v", url, string(bytes), string(bodyString))
 	return err
 }
 
@@ -108,15 +101,10 @@ func (agent *AgentClient) UpdateCanary(globalHeaders *spec.GlobalCanaryHeaders,
 	}
 
 	url := agent.URL + canaryConfigURL
-	resp, err := handleRequest(http.MethodPut, url, bytes)
+	bodyString, err := handleRequest(http.MethodPut, url, bytes)
 	if err != nil {
 		return fmt.Errorf("handleRequest error: %v", err)
 	}
-	bodyBytes, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return fmt.Errorf("read the http body failed: %v", err)
-	}
-	bodyString := string(bodyBytes)
-	logger.Infof("Update Canary, URL: %s, request %s, result: %v", url, string(bytes), bodyString)
+	logger.Infof("Update Canary, URL: %s,request: %s, result: %v", url, string(bytes), string(bodyString))
 	return err
 }
diff --git a/pkg/util/jmxtool/common.go b/pkg/util/jmxtool/common.go
index a74410bd..dd7b8664 100644
--- a/pkg/util/jmxtool/common.go
+++ b/pkg/util/jmxtool/common.go
@@ -95,7 +95,7 @@ type APIErr struct {
 	Message string `yaml:"message"`
 }
 
-func handleRequest(httpMethod string, url string, reqBody []byte) (*http.Response, error) {
+func handleRequest(httpMethod string, url string, reqBody []byte) ([]byte, error) {
 	req, err := http.NewRequest(httpMethod, url, bytes.NewReader(reqBody))
 	if err != nil {
 		return nil, err
@@ -103,27 +103,27 @@ func handleRequest(httpMethod string, url string, reqBody []byte) (*http.Respons
 
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
-		return resp, err
+		return nil, err
 	}
 	defer resp.Body.Close()
 
 	body, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
-		return resp, err
+		return nil, err
 	}
 
-	if !successfulStatusCode(resp.StatusCode) {
-		msg := string(body)
-		apiErr := &APIErr{}
-		err = yaml.Unmarshal(body, apiErr)
-		if err == nil {
-			msg = apiErr.Message
-		}
+	if successfulStatusCode(resp.StatusCode) {
+		return body, nil
+	}
 
-		return resp, fmt.Errorf("Request failed: Code: %d, Msg: %s ", resp.StatusCode, msg)
+	msg := string(body)
+	apiErr := &APIErr{}
+	err = yaml.Unmarshal(body, apiErr)
+	if err == nil {
+		msg = apiErr.Message
 	}
 
-	return resp, nil
+	return nil, fmt.Errorf("Request failed: Code: %d, Msg: %s ", resp.StatusCode, msg)
 }
 
 func successfulStatusCode(code int) bool {
diff --git a/pkg/util/jmxtool/jmx_controller.go b/pkg/util/jmxtool/jmx_controller.go
deleted file mode 100644
index 8cc89c51..00000000
--- a/pkg/util/jmxtool/jmx_controller.go
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package jmxtool
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-)
-
-// JMXRequestOperationType for read, write, exec, list, search
-type JMXRequestOperationType string
-
-const (
-	jmxMbeanAttributeRead  JMXRequestOperationType = "read"
-	jmxMbeanAttributeWrite JMXRequestOperationType = "write"
-	jmxMbeanExec           JMXRequestOperationType = "exec"
-	jmxMbeanList           JMXRequestOperationType = "list"
-	jmxMbeanSearch         JMXRequestOperationType = "search"
-)
-
-// JXMInterface describes all of methods of Mbean operation
-type JXMInterface interface {
-	GetMbeanAttribute(mbean string, attribute string, path string) (interface{}, error)
-	SetMbeanAttribute(mbean string, attribute string, path string, value interface{}) (interface{}, error)
-	ExecuteMbeanOperation(mbean string, operation string, arguments []interface{}) (interface{}, error)
-	ListMbean(mbean string) (interface{}, error)
-	SearchMbeans(pattern string) (interface{}, error)
-}
-
-type (
-	// JolokiaResponse is the response structure
-	JolokiaResponse struct {
-		Status    uint32
-		Timestamp uint32
-		Request   map[string]interface{}
-		Value     interface{}
-		Error     string
-	}
-
-	readRequestBody struct {
-		Type      JMXRequestOperationType `json:"type"`
-		Mbean     string                  `json:"mbean"`
-		Attribute string                  `json:"attribute"`
-		Path      string                  `json:"path"`
-	}
-
-	writeRequestBody struct {
-		Type      JMXRequestOperationType `json:"type"`
-		Mbean     string                  `json:"mbean"`
-		Attribute string                  `json:"attribute"`
-		Path      string                  `json:"path"`
-		Value     interface{}             `json:"value"`
-	}
-
-	executeRequestBody struct {
-		Type      JMXRequestOperationType `json:"type"`
-		Mbean     string                  `json:"mbean"`
-		Operation string                  `json:"operation"`
-		Arguments []interface{}           `json:"arguments"`
-	}
-
-	listRequestBody struct {
-		Type  JMXRequestOperationType `json:"type"`
-		Mbean string                  `json:"mbean"`
-	}
-
-	searchRequestBody struct {
-		Type  JMXRequestOperationType `json:"type"`
-		Mbean string                  `json:"mbean"`
-	}
-
-	// JolokiaClient wraps the URL and HTTPClient
-	JolokiaClient struct {
-		URL        string
-		HTTPClient *http.Client
-	}
-)
-
-// NewJolokiaClient creates Jolokia client
-func NewJolokiaClient(host, port, path string) *JolokiaClient {
-	return &JolokiaClient{
-		"http://" + host + ":" + port + "/" + path + "/",
-		&http.Client{},
-	}
-}
-
-func (client *JolokiaClient) handleRequest(requestBody []byte) (*JolokiaResponse, error) {
-	request, err := http.NewRequest(
-		"POST",
-		client.URL,
-		bytes.NewBuffer(requestBody),
-	)
-	if err != nil {
-		return nil, err
-	}
-	request.Header.Set("Content-Type", "application/json")
-	response, err := client.HTTPClient.Do(request)
-	if err != nil {
-		return nil, err
-	}
-	defer response.Body.Close()
-	contents, err := ioutil.ReadAll(response.Body)
-	if err != nil {
-		return nil, err
-	}
-
-	jolokiaResponse := JolokiaResponse{}
-	err = json.Unmarshal(contents, &jolokiaResponse)
-	if err != nil {
-		return nil, err
-	}
-	if jolokiaResponse.Status != 200 {
-		return nil, fmt.Errorf("status: %d %s", jolokiaResponse.Status, jolokiaResponse.Error)
-	}
-
-	return &jolokiaResponse, nil
-}
-
-func (client *JolokiaClient) execute(requestBody interface{}) (interface{}, error) {
-	var err error
-	body, err := json.Marshal(requestBody)
-	if err != nil {
-		return nil, err
-	}
-	resp, err := client.handleRequest(body)
-	if err != nil {
-		return nil, fmt.Errorf("access jmx failed: %v", err)
-	}
-	return resp.Value, err
-}
-
-// GetMbeanAttribute reads Mbean attribute.
-func (client *JolokiaClient) GetMbeanAttribute(mbean string, attribute string, path string) (interface{}, error) {
-	requestBody := readRequestBody{
-		Mbean:     mbean,
-		Attribute: attribute,
-		Type:      jmxMbeanAttributeRead,
-	}
-	if path != "" {
-		requestBody.Path = path
-	}
-
-	result, err := client.execute(requestBody)
-	if err != nil {
-		return nil, fmt.Errorf("read Mbean Attribute failed: %v", err)
-	}
-	return result, err
-}
-
-// SetMbeanAttribute writes Mbean attribute
-func (client *JolokiaClient) SetMbeanAttribute(mbean string, attribute string, path string, value interface{}) (interface{}, error) {
-	requestBody := writeRequestBody{
-		Mbean:     mbean,
-		Attribute: attribute,
-		Type:      jmxMbeanAttributeWrite,
-		Value:     value,
-	}
-	if path != "" {
-		requestBody.Path = path
-	}
-
-	result, err := client.execute(requestBody)
-	if err != nil {
-		return nil, fmt.Errorf("write Mbean Attribute failed: %v", err)
-	}
-	return result, nil
-}
-
-// ExecuteMbeanOperation executes Mbean operation
-func (client *JolokiaClient) ExecuteMbeanOperation(mbean string, operation string, arguments []interface{}) (interface{}, error) {
-	requestBody := executeRequestBody{
-		Mbean:     mbean,
-		Operation: operation,
-		Type:      jmxMbeanExec,
-		Arguments: arguments,
-	}
-
-	result, err := client.execute(requestBody)
-	if err != nil {
-		return nil, fmt.Errorf("execute Mbean Operation failed: %v", err)
-	}
-	return result, nil
-}
-
-// ListMbean lists the Mbean
-func (client *JolokiaClient) ListMbean(mbean string) (interface{}, error) {
-	requestBody := listRequestBody{
-		Type:  jmxMbeanList,
-		Mbean: mbean,
-	}
-
-	result, err := client.execute(requestBody)
-	if err != nil {
-		return nil, fmt.Errorf("list Mbean failed: %v", err)
-	}
-	return result, nil
-}
-
-// SearchMbeans searches Mbean
-func (client *JolokiaClient) SearchMbeans(pattern string) (interface{}, error) {
-	requestBody := searchRequestBody{
-		Type:  jmxMbeanSearch,
-		Mbean: pattern,
-	}
-
-	result, err := client.execute(requestBody)
-	if err != nil {
-		return nil, fmt.Errorf("search Mbean failed: %v", err)
-	}
-	return result, nil
-}


Test patch:
diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go
index 182b2e4f..4ecfbc32 100644
--- a/pkg/cluster/cluster_test.go
+++ b/pkg/cluster/cluster_test.go
@@ -22,10 +22,14 @@ import (
 	"sync"
 	"testing"
 	"time"
+
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	clientv3 "go.etcd.io/etcd/client/v3"
 )
 
 func mockClusters(count int) []*cluster {
 	opts, _, _ := mockMembers(count)
+
 	clusters := make([]*cluster, count)
 
 	bootCluster, err := New(opts[0])
@@ -65,7 +69,7 @@ func mockClusters(count int) []*cluster {
 		c := cls.(*cluster)
 
 		for {
-			err = c.getReady()
+			_, err := c.getClient()
 			time.Sleep(HeartbeatInterval)
 			if err != nil {
 				continue
@@ -90,6 +94,383 @@ func closeClusters(clusters []*cluster) {
 }
 
 func TestCluster(t *testing.T) {
-	clusters := mockClusters(5)
+	clusters := mockClusters(3)
 	defer closeClusters(clusters)
+	// for testing longRequestContext()
+	clusters[0].longRequestContext()
+
+}
+
+func TestLease(t *testing.T) {
+	_, err := strToLease("266394")
+	if err != nil {
+		t.Errorf("str to lease failed: %v", err)
+	}
+}
+
+func TestLeaseInvalid(t *testing.T) {
+	_, err := strToLease("test")
+	if err == nil {
+		t.Errorf("str to lease should not succ with \"test\" value")
+	}
+}
+
+func TestClusterStart(t *testing.T) {
+	opts, _, _ := mockMembers(1)
+
+	cls, err := New(opts[0])
+
+	if err != nil {
+		t.Errorf("init failed: %v", err)
+	}
+
+	c := cls.(*cluster)
+
+	_, _, err = c.StartServer()
+
+	if err != nil {
+		t.Errorf("start server failed, %v", err)
+	}
+}
+
+func TestClusterPurgeMember(t *testing.T) {
+	opts, _, _ := mockMembers(2)
+
+	cls, err := New(opts[0])
+
+	if err != nil {
+		t.Errorf("init failed: %v", err)
+	}
+
+	c := cls.(*cluster)
+	err = c.PurgeMember("no-member")
+	if err == nil {
+		t.Errorf("purge a none exit member, should be failed")
+	}
+}
+
+func TestClusterSyncer(t *testing.T) {
+	opts, _, _ := mockMembers(1)
+	cls, err := New(opts[0])
+
+	if err != nil {
+		t.Errorf("init failed: %v", err)
+	}
+
+	c := cls.(*cluster)
+
+	_, err = c.getClient()
+	if err != nil {
+		t.Errorf("get ready failed: %v", err)
+	}
+
+	syncer, err := c.Syncer(3 * time.Second)
+
+	if err != nil {
+		t.Errorf("new syncer failed: %v", err)
+	}
+
+	schan, err := syncer.Sync("/akey")
+	if err != nil {
+		t.Errorf("syncer sync failed: %v", err)
+	}
+
+	rschan, err := syncer.SyncRaw("/akey")
+	if err != nil {
+		t.Errorf("syncer sync failed: %v", err)
+	}
+
+	c.Put("/akey", "avalue")
+
+	for {
+		var value *string
+		select {
+		case value = <-schan:
+			fmt.Printf("sync value is %v\n", value)
+		}
+		break
+	}
+
+	for {
+		var value *mvccpb.KeyValue
+		select {
+		case value = <-rschan:
+			fmt.Printf("sync raw value is %v\n", value)
+		}
+		break
+	}
+
+	pchan, err := syncer.SyncPrefix("/abcd")
+	if err != nil {
+		t.Errorf("syncer sync failed: %v", err)
+	}
+
+	rpchan, err := syncer.SyncRawPrefix("/abcd")
+	if err != nil {
+		t.Errorf("syncer sync failed: %v", err)
+	}
+
+	c.Put("/abcd/efg", "yoyo")
+
+	if _, err = c.GetPrefix("/abcd"); err != nil {
+		t.Errorf("cluster get prefix failed: %v", err)
+	}
+
+	for {
+		value := make(map[string]string, 1)
+		select {
+		case value = <-pchan:
+			fmt.Printf("sync prefix value is %v\n", value)
+		}
+		break
+	}
+
+	for {
+		value := make(map[string]*mvccpb.KeyValue)
+		select {
+		case value = <-rpchan:
+			fmt.Printf("sync raw pvalue is %v\n", value)
+		}
+		break
+	}
+
+	syncer.Close()
+
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+	cls.CloseServer(wg)
+	wg.Wait()
+}
+
+func TestClusterWatcher(t *testing.T) {
+	opts, _, _ := mockMembers(1)
+	cls, err := New(opts[0])
+
+	if err != nil {
+		t.Errorf("init failed: %v", err)
+	}
+
+	c := cls.(*cluster)
+
+	_, err = c.getClient()
+	if err != nil {
+		t.Errorf("get ready failed: %v", err)
+	}
+
+	watcher, err := c.Watcher()
+
+	if err != nil {
+		t.Errorf("new syncer failed: %v", err)
+	}
+
+	wchan, err := watcher.Watch("/akey/value")
+	if err != nil {
+		t.Errorf("watcher watch failed: %v", err)
+	}
+
+	rchan, err := watcher.WatchRaw("/akey/value")
+	if err != nil {
+		t.Errorf("watcher watch failed: %v", err)
+	}
+
+	c.Put("/akey/value", "yes")
+
+	for {
+		var value *string
+		select {
+		case value = <-wchan:
+			fmt.Printf("watch value is %v\n", value)
+		}
+		break
+	}
+
+	for {
+		var value *clientv3.Event
+		select {
+		case value = <-rchan:
+			fmt.Printf("watch raw value is %v\n", value)
+		}
+		break
+	}
+
+	pchan, err := watcher.WatchPrefix("/ab")
+	if err != nil {
+		t.Errorf("watcher watch failed: %v", err)
+	}
+
+	rawpchan, err := watcher.WatchRawPrefix("/abcd")
+	if err != nil {
+
+		t.Errorf("watcher watch failed: %v", err)
+	}
+
+	c.Put("/abc", "kkk")
+
+	for {
+		value := make(map[string]*string, 1)
+		select {
+		case value = <-pchan:
+			fmt.Printf("watch prefix value is %v\n", value)
+		}
+		break
+	}
+	c.Put("/abcd/ef", "jjj")
+
+	for {
+		value := make(map[string]*clientv3.Event, 1)
+		select {
+		case value = <-rawpchan:
+			fmt.Printf("watch prefix raw value is %v\n", value)
+		}
+		break
+	}
+	c.DeletePrefix("/abc")
+
+	for {
+		value := make(map[string]*string, 1)
+		select {
+		case value = <-pchan:
+			fmt.Printf("watch delete prefix value is %v\n", value)
+		}
+		break
+	}
+
+	c.DeletePrefix("/abcd/")
+
+	for {
+		value := make(map[string]*clientv3.Event, 1)
+		select {
+		case value = <-rawpchan:
+			fmt.Printf("watch delete prefix raw value is %v\n", value)
+		}
+		break
+	}
+
+	watcher.Close()
+
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+	cls.CloseServer(wg)
+	wg.Wait()
+}
+
+func TestUtil(t *testing.T) {
+	equal := isDataEuqal(map[string]*mvccpb.KeyValue{
+		"aaa": {
+			Key:     []byte("akey"),
+			Version: 11233,
+		},
+	}, map[string]*mvccpb.KeyValue{
+		"aaa": {
+			Key:     []byte("akey"),
+			Version: 11233,
+		},
+	})
+
+	if !equal {
+		t.Error("isDataEqual failed")
+	}
+
+	equal = isDataEuqal(map[string]*mvccpb.KeyValue{
+		"aaa": {
+			Key:     []byte("akey"),
+			Version: 11233,
+		},
+		"bbb": {
+			Key:     []byte("akey"),
+			Version: 11233,
+		},
+	}, map[string]*mvccpb.KeyValue{
+		"aaa": {
+			Key:     []byte("akey"),
+			Version: 11233,
+		},
+	})
+
+	if equal {
+		t.Error("isDataEqual should not equal failed")
+	}
+}
+
+func TestMutexAndOP(t *testing.T) {
+	opts, _, _ := mockMembers(1)
+	cls, err := New(opts[0])
+
+	if err != nil {
+		t.Errorf("init failed: %v", err)
+	}
+
+	c := cls.(*cluster)
+
+	_, err = c.getClient()
+	if err != nil {
+		t.Errorf("get ready failed: %v", err)
+	}
+
+	m, err := c.Mutex("akey")
+	if err != nil {
+		t.Errorf("cluster mutex failed: %v", err)
+	}
+
+	m.Lock()
+	defer m.Unlock()
+
+	value := "a value"
+	err = c.PutAndDeleteUnderLease(map[string]*string{
+		"akey": &value,
+	})
+
+	if err != nil {
+		t.Errorf("PutAndDeleteUnderLease failed: %v", err)
+	}
+
+	err = c.PutAndDelete(map[string]*string{
+		"/test/akey": &value,
+	})
+
+	if err != nil {
+		t.Errorf("PutAndDelete failed :%v", err)
+	}
+
+	if err = c.Delete("/test/akey"); err != nil {
+		t.Errorf("Delete failed: %v", err)
+	}
+
+	err = c.DeletePrefix("akey")
+	if err != nil {
+		t.Errorf("DeletePrefix failed: %v", err)
+	}
+}
+
+func TestUtilEqual(t *testing.T) {
+	equal := isKeyValueEqual(&mvccpb.KeyValue{
+		Key: []byte("abc"),
+	}, &mvccpb.KeyValue{
+		Key: []byte("abc"),
+	})
+
+	if !equal {
+		t.Error("isKeyValueEqual invalid")
+	}
+
+	equal = isKeyValueEqual(nil, &mvccpb.KeyValue{
+		Key: []byte("abc"),
+	})
+
+	if equal {
+		t.Error("isKeyValueEqual invalid, should not equal")
+	}
+
+	equal = isKeyValueEqual(&mvccpb.KeyValue{
+		Key: []byte("abc"),
+	}, nil)
+
+	if equal {
+		t.Error("isKeyValueEqual invalid, should not equal")
+	}
+
+	equal = isKeyValueEqual(nil, nil)
+	if !equal {
+		t.Error("isKeyValueEqual invalid, should equal")
+	}
 }
diff --git a/pkg/cluster/layout_test.go b/pkg/cluster/layout_test.go
new file mode 100644
index 00000000..2f5e30fc
--- /dev/null
+++ b/pkg/cluster/layout_test.go
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cluster
+
+import (
+	"testing"
+)
+
+func TestLayout(t *testing.T) {
+	l := Layout{}
+
+	if len(l.OtherLease("member-1")) == 0 {
+		t.Error("other lease  empty")
+	}
+
+	if len(l.StatusObjectPrefix("member-1")) == 0 {
+		t.Error("other lease  empty")
+	}
+
+	if len(l.StatusMemberPrefix()) == 0 {
+		t.Error("StatusMemberPrefix  empty")
+	}
+
+	if len(l.OtherStatusMemberKey("member-1")) == 0 {
+		t.Error("other status member key empty")
+	}
+
+	if len(l.StatusObjectsPrefix()) == 0 {
+		t.Error("StatusObjectsPrefix empty")
+	}
+
+	if len(l.StatusObjectKey("member-1")) == 0 {
+		t.Error("StatusObjectKey key empty")
+	}
+
+	if len(l.ConfigObjectPrefix()) == 0 {
+		t.Error("ConfigObjectPrefix empty")
+	}
+
+	if len(l.ConfigObjectKey("member-1")) == 0 {
+		t.Error("ConfigObjectKey empty")
+	}
+
+	if len(l.ConfigVersion()) == 0 {
+		t.Error("ConfigVersion empty")
+	}
+
+	if len(l.WasmCodeEvent()) == 0 {
+		t.Error("WasmCodeEvent empty")
+	}
+}
diff --git a/pkg/cluster/member_test.go b/pkg/cluster/member_test.go
index 6384ee54..177d7eab 100644
--- a/pkg/cluster/member_test.go
+++ b/pkg/cluster/member_test.go
@@ -19,12 +19,14 @@ package cluster
 
 import (
 	"fmt"
-	"io/ioutil"
+	"math/rand"
 	"os"
+	"path"
 	"path/filepath"
 	"reflect"
 	"sort"
 	"testing"
+	"time"
 
 	"github.com/phayes/freeport"
 	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
@@ -39,21 +41,20 @@ var (
 	tempDir       = os.TempDir()
 )
 
-func TestMain(m *testing.M) {
-	absLogDir, err := ioutil.TempDir(tempDir, "global-log")
-	if err != nil {
-		panic(fmt.Errorf("create tmp dir failed: %v", err))
-	}
+func init() {
+	rand.Seed(time.Now().UnixNano())
+}
 
-	logger.Init(&option.Options{
-		Name:      "member-for-log",
-		AbsLogDir: absLogDir,
-	})
+func getRandomString(n int) string {
+	randBytes := make([]byte, n/2)
+	rand.Read(randBytes)
+	return fmt.Sprintf("%x", randBytes)
+}
 
+func TestMain(m *testing.M) {
+	logger.InitNop()
+	tempDir = path.Join(tempDir, getRandomString(6))
 	code := m.Run()
-
-	logger.Sync()
-	os.RemoveAll(tempDir)
 	os.Exit(code)
 }
 
@@ -117,6 +118,18 @@ func mockMembers(count int) ([]*option.Options, membersSlice, []*pb.Member) {
 
 	sort.Sort(members)
 
+	tmp := members.copy()
+	if len(tmp) == 0 {
+		panic("members copy failed")
+	}
+	noexistMember := members.getByPeerURL("no-exist")
+
+	if noexistMember != nil {
+		panic("get a member not exist succ, should failed")
+	}
+
+	members.deleteByName("no-exist")
+	members.deleteByPeerURL("no-exist-purl")
 	return opts, members, pbMembers
 }
 
diff --git a/pkg/common/callback_test.go b/pkg/common/callback_test.go
deleted file mode 100644
index 40625a0f..00000000
--- a/pkg/common/callback_test.go
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-import (
-	"fmt"
-	"testing"
-)
-
-func createFn(name string) func() string {
-	return func() string {
-		fmt.Println("I am a callback - " + name)
-		return name
-	}
-}
-func TestCallBack(t *testing.T) {
-	foo := createFn("foo")
-	bar := createFn("bar")
-	name := "test"
-	cb := NewNamedCallback(name, foo)
-	if cb.Name() != name {
-		t.Errorf("expected %s, result %s", name, cb.Name())
-	}
-
-	type FuncType func() string
-	cbfn := cb.Callback()
-	if f, ok := cbfn.(func() string); ok {
-		if foo() != FuncType(f)() {
-			t.Errorf("error")
-		}
-	}
-
-	cb.SetCallback(bar)
-	cbfn = cb.Callback()
-	if f, ok := cbfn.(func() string); ok {
-		if bar() != FuncType(f)() {
-			t.Errorf("error")
-		}
-	}
-}
-
-func TestCallBackSet(t *testing.T) {
-	set := NewNamedCallbackSet()
-
-	x := createFn("XXX")
-	y := createFn("YYY")
-	z := createFn("ZZZ")
-	a := createFn("AAA")
-	b := createFn("BBB")
-
-	AddCallback(set, "XXX", x, NormalPriorityCallback)
-	AddCallback(set, "YYY", y, NormalPriorityCallback)
-	AddCallback(set, "ZZZ", z, CriticalPriorityCallback)
-	AddCallback(set, "AAA", a, "ZZZ")
-	AddCallback(set, "BBB", b, "AAA")
-
-	type FuncType func() string
-	for _, cb := range set.CopyCallbacks() {
-		if f, ok := cb.callback.(func() string); ok {
-			if cb.name != FuncType(f)() {
-				t.Errorf("expected %s, result: %s", cb.name, FuncType(f)())
-			} else {
-				DeleteCallback(set, cb.name)
-			}
-
-		}
-	}
-
-	if DeleteCallback(nil, "") != nil {
-		t.Errorf("delete the nil set should not be OK")
-	}
-	DeleteCallback(set, "ABC")
-
-	n := len(set.GetCallbacks())
-	if n > 0 {
-		t.Errorf("expected: 0, result:%d", n)
-	}
-}
diff --git a/pkg/common/os_test.go b/pkg/common/os_test.go
new file mode 100644
index 00000000..6de5630d
--- /dev/null
+++ b/pkg/common/os_test.go
@@ -0,0 +1,68 @@
+// +build !windows
+
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"bytes"
+	"os"
+	"os/exec"
+	"testing"
+)
+
+func TestNonZeroExit(t *testing.T) {
+	if os.Getenv("BE_TestNonZeroExit") == "1" {
+		Exit(1, "error")
+		return
+	}
+
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+
+	cmd := exec.Command(os.Args[0], "-test.run=TestNonZeroExit")
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	cmd.Env = append(os.Environ(), "BE_TestNonZeroExit=1")
+	err := cmd.Run()
+	e, ok := err.(*exec.ExitError)
+	if ok && !e.Success() && stderr.String() != "" && stdout.String() == "" {
+		return
+	}
+	t.Fatalf("process ran with err %v, want exit status 1", err)
+}
+
+func TestZeroExit(t *testing.T) {
+	if os.Getenv("BE_TestZeroExit") == "1" {
+		Exit(0, "everythingok")
+		return
+	}
+
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+
+	cmd := exec.Command(os.Args[0], "-test.run=TestZeroExit")
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	cmd.Env = append(os.Environ(), "BE_TestZeroExit=1")
+	err := cmd.Run()
+	if err == nil && stdout.String() != "" && stderr.String() == "" {
+		return
+	}
+	t.Fatalf("process ran with err %v, want exit status 0", err)
+}
diff --git a/pkg/common/signal_unix_test.go b/pkg/common/signal_unix_test.go
new file mode 100644
index 00000000..295c9b01
--- /dev/null
+++ b/pkg/common/signal_unix_test.go
@@ -0,0 +1,70 @@
+// +build !windows
+
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"syscall"
+	"testing"
+)
+
+func TestNotifySignalAndRaiseSignal(t *testing.T) {
+	c := make(chan Signal, 5)
+	var currSig Signal
+	sigs := []Signal{SignalInt, SignalTerm, SingalUsr2}
+
+	// NotifySignal
+	if err := NotifySignal(nil); err == nil {
+		t.Errorf("error for nil chan1")
+	}
+	if err := NotifySignal(nil, SignalInt, SignalTerm); err == nil {
+		t.Errorf("error for nil chan2")
+	}
+	if err := NotifySignal(c); err == nil {
+		t.Errorf("error for empty signal")
+	}
+	if err := NotifySignal(c, SignalInt, "BadSignal", SignalTerm); err == nil {
+		t.Errorf("error for unsupported signal")
+	}
+
+	if err := NotifySignal(c, sigs...); err != nil {
+		t.Fatalf("error with input %+v", sigs)
+	}
+	for _, s := range sigs {
+		syscall.Kill(syscall.Getpid(), signalToOsMap[s])
+		currSig = <-c
+		if currSig != s {
+			t.Fatalf("error signal from chan")
+		}
+	}
+
+	// RaiseSignal
+	if err := RaiseSignal(syscall.Getpid(), "BadSignal"); err == nil {
+		t.Errorf("error with wrong input")
+	}
+	for _, s := range sigs {
+		if err := RaiseSignal(syscall.Getpid(), s); err != nil {
+			t.Fatalf("error raise signal")
+		}
+		currSig = <-c
+		if currSig != s {
+			t.Fatalf("error signal from chan")
+		}
+	}
+}
diff --git a/pkg/common/stat_aggregator_test.go b/pkg/common/stat_aggregator_test.go
deleted file mode 100644
index 71f134b4..00000000
--- a/pkg/common/stat_aggregator_test.go
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package common
-
-import (
-	"fmt"
-	"reflect"
-	"testing"
-)
-
-type errMapper struct {
-	errs  []error
-	index int
-}
-
-func newErrMapper(errs []error) *errMapper { return &errMapper{errs: errs} }
-
-func (em *errMapper) mapNext(t *testing.T, err error) {
-	if em.index >= len(em.errs) {
-		t.Fatalf("BUG: map beyond existent entries")
-	}
-
-	var (
-		want = "<nil>"
-		got  = "<nil>"
-	)
-
-	switch {
-	case em.errs[em.index] == nil && err == nil:
-		em.index++
-		return
-	case em.errs[em.index] != nil && err == nil:
-		want = em.errs[em.index].Error()
-	case em.errs[em.index] == nil && err != nil:
-		got = err.Error()
-	case em.errs[em.index] != nil && err != nil:
-		want = em.errs[em.index].Error()
-		got = err.Error()
-		if want == got {
-			em.index++
-			return
-		}
-	}
-
-	t.Fatalf("map failed in [%d]:\nwant: %s\ngot : %s", em.index, want, got)
-}
-
-func mockUnsupportedKindError(got interface{}) error {
-	return fmt.Errorf("unsupported kind %s", reflect.ValueOf(got).Kind())
-}
-
-func mockWantXGotYKindError(want numericKind, got interface{}) error {
-	return fmt.Errorf("want kind %s, got %s",
-		want,
-		reflect.ValueOf(got).Kind())
-}
-
-func TestNumericMaxAggregator(t *testing.T) {
-	em := newErrMapper([]error{
-		mockUnsupportedKindError(""),
-		nil,
-		nil,
-		mockWantXGotYKindError(intNum, uint(0)),
-		nil,
-		nil,
-		mockWantXGotYKindError(intNum, float64(0.0)),
-	})
-
-	a := &NumericMaxAggregator{}
-	em.mapNext(t, a.Aggregate(""))
-	em.mapNext(t, a.Aggregate(int(2)))
-	em.mapNext(t, a.Aggregate(int(-2)))
-	em.mapNext(t, a.Aggregate(uint(10)))
-	em.mapNext(t, a.Aggregate(int(10)))
-	em.mapNext(t, a.Aggregate(int(-2)))
-
-	em.mapNext(t, a.Aggregate(float64(2.0)))
-
-	wantResult := int64(10)
-	gotResult := a.Result().(int64)
-	if gotResult != wantResult {
-		t.Fatalf("want %d, got %d", wantResult, gotResult)
-	}
-
-	name := "numeric_max"
-	if a.String() != name {
-		t.Errorf("want %s, got %s", "numeric_max", name)
-	}
-}
-
-func TestNumericMinAggregator(t *testing.T) {
-	em := newErrMapper([]error{
-		nil,
-		mockWantXGotYKindError(floatNum, int(-2)),
-		mockWantXGotYKindError(floatNum, uint(10)),
-		nil,
-		nil,
-		mockUnsupportedKindError([]float64{-2.3}),
-	})
-
-	a := &NumericMinAggregator{}
-	em.mapNext(t, a.Aggregate(float64(2.3)))
-	em.mapNext(t, a.Aggregate(int(-2)))
-	em.mapNext(t, a.Aggregate(uint(10)))
-	em.mapNext(t, a.Aggregate(float32(-100)))
-	em.mapNext(t, a.Aggregate(float64(100)))
-	em.mapNext(t, a.Aggregate([]float64{-2.3}))
-
-	wantResult := float64(-100)
-	gotResult := a.Result().(float64)
-	if gotResult != wantResult {
-		t.Fatalf("want %v, got %v", wantResult, gotResult)
-	}
-
-	name := "numeric_min"
-	if a.String() != name {
-		t.Errorf("want %s, got %s", "numeric_max", name)
-	}
-}
-
-func TestNumericSumAggregator(t *testing.T) {
-	em := newErrMapper([]error{
-		mockUnsupportedKindError(map[int]int{0: 1}),
-		nil,
-		mockWantXGotYKindError(uintNum, int(-2)),
-		nil,
-		mockWantXGotYKindError(uintNum, float32(10.1)),
-		nil,
-	})
-
-	a := &NumericSumAggregator{}
-	em.mapNext(t, a.Aggregate(map[int]int{0: 1}))
-	em.mapNext(t, a.Aggregate(uint(2)))
-	em.mapNext(t, a.Aggregate(int(-2)))
-	em.mapNext(t, a.Aggregate(uint(10)))
-	em.mapNext(t, a.Aggregate(float32(10.1)))
-	em.mapNext(t, a.Aggregate(uint(0)))
-
-	wantResult := uint64(12)
-	gotResult := a.Result().(uint64)
-	if gotResult != wantResult {
-		t.Fatalf("want %d, got %d", wantResult, gotResult)
-	}
-
-	name := "numeric_sum"
-	if a.String() != name {
-		t.Errorf("want %s, got %s", "numeric_max", name)
-	}
-}
-
-func TestNumericAvgAggregator(t *testing.T) {
-	em := newErrMapper([]error{
-		mockUnsupportedKindError(make(chan int)),
-		nil,
-		nil,
-		mockWantXGotYKindError(intNum, float32(-10.0)),
-		mockWantXGotYKindError(intNum, uint(100)),
-		nil,
-		nil,
-		nil,
-	})
-
-	a := &NumericAvgAggregator{}
-	em.mapNext(t, a.Aggregate(make(chan int)))
-	em.mapNext(t, a.Aggregate(int(2)))
-	em.mapNext(t, a.Aggregate(int(-2)))
-	em.mapNext(t, a.Aggregate(float32(-10.0)))
-	em.mapNext(t, a.Aggregate(uint(100)))
-	em.mapNext(t, a.Aggregate(int(-10)))
-	em.mapNext(t, a.Aggregate(int(0)))
-	em.mapNext(t, a.Aggregate(int(110)))
-
-	wantResult := int64(20)
-	gotResult := a.Result().(int64)
-	if gotResult != wantResult {
-		t.Fatalf("want %d, got %d", wantResult, gotResult)
-	}
-
-	name := "numeric_average"
-	if a.String() != name {
-		t.Errorf("want %s, got %s", "numeric_max", name)
-	}
-}
diff --git a/pkg/common/utils_test.go b/pkg/common/utils_test.go
index f4dbf830..3ed6eb75 100644
--- a/pkg/common/utils_test.go
+++ b/pkg/common/utils_test.go
@@ -18,368 +18,14 @@
 package common
 
 import (
+	"crypto/rand"
 	"fmt"
-	"math"
+	"io"
+	"path/filepath"
+	"runtime"
 	"testing"
 )
 
-func TestScanTokensNormally(t *testing.T) {
-	ret, err := ScanTokens(`abcdef`, true, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abcdef` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def\}ghi`, false, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc\{def\}ghi` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def\}ghi`, true, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc{def}ghi` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	visitor := func(pos int, token string) (bool, string) {
-		if pos != 13 {
-			t.Fatalf("wrong token position: %d", pos)
-		}
-		if token != `jkl` {
-			t.Fatalf("wrong token: %s", token)
-		}
-
-		return true, `JKL`
-	}
-
-	ret, err = ScanTokens(`abc\{def\}ghi{jkl}`, true, visitor)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc{def}ghiJKL` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def\}ghi{jkl}`, false, visitor)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc\{def\}ghiJKL` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def\}ghi{jkl}lmn`, true, visitor)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc{def}ghiJKLlmn` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def\}ghi{jkl}\{lmn\}`, false, visitor)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc\{def\}ghiJKL\{lmn\}` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	// escape char out of token
-
-	ret, err = ScanTokens(`abc\{def`, true, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc{def` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def`, false, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc\{def` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def\{ghi`, true, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc{def{ghi` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\}def`, true, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc}def` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\}def`, false, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc\}def` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\}def\}ghi\{`, true, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc}def}ghi{` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	// escape char in token
-
-	ret, err = ScanTokens(`abc\{defghi{\{jkl\{}`, true, nil)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc{defghi{{jkl{}` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	visitor = func(pos int, token string) (bool, string) {
-		if pos != 11 {
-			t.Fatalf("wrong token position: %d", pos)
-		}
-		if token != `{jkl{` {
-			t.Fatalf("wrong token: %s", token)
-		}
-
-		return true, `\{JKL\}`
-	}
-
-	ret, err = ScanTokens(`abc\{defghi{\{jkl\{}`, true, visitor)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc{defghi{JKL}` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{defghi{\{jkl\{}`, false, visitor)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `abc\{defghi\{JKL\}` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	visitor = func(_ int, token string) (bool, string) {
-		if token != `hello` {
-			t.Fatalf("wrong token: %s", token)
-		}
-
-		return true, `world`
-	}
-
-	ret, err = ScanTokens(`\{hello\} - {hello}`, true, visitor)
-	if err != nil {
-		t.Fatalf("unexpected error")
-	}
-	if ret != `{hello} - world` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-}
-
-func TestScanTokensExceptionally(t *testing.T) {
-	ret, err := ScanTokens(`abc{def`, true, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc{def` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc}def`, true, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc}def` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def{ghi`, false, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc\{def{ghi` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def}ghi`, true, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc\{def}ghi` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc}def}ghi`, true, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc}def}ghi` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc{def{ghi`, true, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc{def{ghi` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\}def\{ghi{`, false, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc\}def\{ghi{` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def\}ghi}`, true, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc\{def\}ghi}` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-
-	ret, err = ScanTokens(`abc\{def\}{\{ghi\{{jkl}}`, true, nil)
-	if err == nil {
-		t.Fatalf("expected error unraied")
-	}
-	if ret != `abc\{def\}{\{ghi\{{jkl}}` {
-		t.Fatalf("scan token returns wrong result: %s", ret)
-	}
-}
-
-func TestNextNumberPowerOf2(t *testing.T) {
-	ret := NextNumberPowerOf2(0)
-	if ret != 0 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-
-	ret = NextNumberPowerOf2(1)
-	if ret != 1 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-
-	ret = NextNumberPowerOf2(2)
-	if ret != 2 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-
-	ret = NextNumberPowerOf2(8)
-	if ret != 8 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-
-	ret = NextNumberPowerOf2(1023)
-	if ret != 1024 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-
-	ret = NextNumberPowerOf2(math.MaxUint32)
-	if ret != math.MaxUint32+1 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-
-	ret = NextNumberPowerOf2(math.MaxUint32 + 1)
-	if ret != math.MaxUint32+1 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-
-	ret = NextNumberPowerOf2(1<<63 - 1)
-	if ret != 1<<63 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-
-	ret = NextNumberPowerOf2(1 << 63)
-	if ret != 1<<63 {
-		t.Fatalf("unexpected return: %d", ret)
-	}
-}
-
-func TestGraphiteSplit(t *testing.T) {
-	str := "4.name#3.age#5.email"
-	strarr := GraphiteSplit(str, ".", "#")
-
-	if len(strarr) != 3 || strarr[0] != "name" || strarr[1] != "age" || strarr[2] != "email" {
-		t.Errorf("%v\n", strarr)
-	}
-
-	str = "5.name"
-	strarr = GraphiteSplit(str, ".", "#")
-	if len(strarr) != 0 {
-		t.Errorf("%v\n", strarr)
-	}
-
-	str = "name"
-	strarr = GraphiteSplit(str, ".", "#")
-	if len(strarr) != 0 {
-		t.Errorf("%v\n", strarr)
-	}
-
-	str = "4.email"
-	strarr = GraphiteSplit(str, ".", "#")
-	if strarr[0] != "emai" {
-		t.Errorf("%v\n", strarr)
-	}
-}
-
-func TestPanicToErr(t *testing.T) {
-	errstr := "panic"
-	f := func() { panic(errstr) }
-	var e error
-	PanicToErr(f, &e)
-	if e.Error() != errstr {
-		t.Errorf("expected %s, result %s", errstr, e.Error())
-	}
-
-	f = func() { panic(fmt.Errorf(errstr)) }
-	PanicToErr(f, &e)
-	if e.Error() != errstr {
-		t.Errorf("expected %s, result %s", errstr, e.Error())
-	}
-
-	f = func() { panic(nil) }
-	PanicToErr(f, nil)
-}
-
-func TestRemoveRepeatedByteTest(t *testing.T) {
-	s := "abbcccddd"
-	var b byte = 'b'
-	result := RemoveRepeatedByte(s, b)
-	expected := "abcccddd"
-	if result != expected {
-		t.Errorf("expected %s, result %s", expected, result)
-	}
-
-	s = "a"
-	b = 'a'
-	result = RemoveRepeatedByte(s, b)
-	expected = "a"
-	if result != expected {
-		t.Errorf("expected %s, result %s", expected, result)
-	}
-}
-
 func TestValidateName(t *testing.T) {
 	if err := ValidateName("localhost"); err != nil {
 		t.Errorf("error %v", err)
@@ -392,8 +38,21 @@ func TestValidateName(t *testing.T) {
 	}
 }
 
+func uuid() (string, error) {
+	uuid := make([]byte, 16)
+	n, err := io.ReadFull(rand.Reader, uuid)
+	if n != len(uuid) || err != nil {
+		return "", err
+	}
+
+	uuid[8] = uuid[8]&^0xc0 | 0x80
+	uuid[6] = uuid[6]&^0xf0 | 0x40
+
+	return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
+}
+
 func TestDirFunc(t *testing.T) {
-	uuid, err := UUID()
+	uuid, err := uuid()
 	if err != nil {
 		t.Errorf("%v", err)
 	}
@@ -416,3 +75,33 @@ func TestDirFunc(t *testing.T) {
 	}
 
 }
+
+func TestIsDirEmpty(t *testing.T) {
+	name, _ := uuid()
+	if flag := IsDirEmpty(name); !flag {
+		t.Errorf("not exist dir is empty")
+	}
+}
+
+func TestExpandDir(t *testing.T) {
+	if !filepath.IsAbs(ExpandDir("testExpandDir")) {
+		t.Errorf("should return abs path")
+	}
+}
+
+func TestBackupAndCleanDir(t *testing.T) {
+	if BackupAndCleanDir("notexistdir") != nil {
+		t.Errorf("nil for not exist dir")
+	}
+}
+
+func TestNormalizeZapLogPath(t *testing.T) {
+	if runtime.GOOS != "windows" {
+		for i := 0; i < 10; i++ {
+			name, _ := uuid()
+			if NormalizeZapLogPath(name) != name {
+				t.Errorf("for non-windows should return same result")
+			}
+		}
+	}
+}
diff --git a/pkg/context/contexttest/context.go b/pkg/context/contexttest/context.go
new file mode 100644
index 00000000..8eb57bd3
--- /dev/null
+++ b/pkg/context/contexttest/context.go
@@ -0,0 +1,252 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package contexttest
+
+import (
+	"sync"
+	"time"
+
+	"github.com/megaease/easegress/pkg/context"
+	"github.com/megaease/easegress/pkg/tracing"
+	"github.com/megaease/easegress/pkg/util/httpstat"
+	"github.com/megaease/easegress/pkg/util/texttemplate"
+)
+
+// MockedHTTPContext is the mocked HTTP context
+type MockedHTTPContext struct {
+	lock                     sync.Mutex
+	finishFuncs              []func()
+	MockedLock               func()
+	MockedUnlock             func()
+	MockedSpan               func() tracing.Span
+	MockedRequest            MockedHTTPRequest
+	MockedResponse           MockedHTTPResponse
+	MockedDeadline           func() (time.Time, bool)
+	MockedDone               func() <-chan struct{}
+	MockedErr                func() error
+	MockedValue              func(key interface{}) interface{}
+	MockedCancel             func(err error)
+	MockedCancelled          func() bool
+	MockedClientDisconnected func() bool
+	MockedDuration           func() time.Duration
+	MockedOnFinish           func(func())
+	MockedAddTag             func(tag string)
+	MockedStatMetric         func() *httpstat.Metric
+	MockedLog                func() string
+	MockedFinish             func()
+	MockedTemplate           func() texttemplate.TemplateEngine
+	MockedSetTemplate        func(ht *context.HTTPTemplate)
+	MockedSaveReqToTemplate  func(filterName string) error
+	MockedSaveRspToTemplate  func(filterName string) error
+	MockedCallNextHandler    func(lastResult string) string
+	MockedSetHandlerCaller   func(caller context.HandlerCaller)
+}
+
+// Lock mocks the Lock function of HTTPContext
+func (c *MockedHTTPContext) Lock() {
+	if c.MockedLock != nil {
+		c.MockedLock()
+	}
+}
+
+// Unlock mocks the Unlock function of HTTPContext
+func (c *MockedHTTPContext) Unlock() {
+	if c.MockedUnlock != nil {
+		c.MockedUnlock()
+	}
+}
+
+// Span mocks the Span function of HTTPContext
+func (c *MockedHTTPContext) Span() tracing.Span {
+	if c.MockedSpan != nil {
+		return c.MockedSpan()
+	}
+	return tracing.NewSpan(tracing.NoopTracing, "mocked")
+}
+
+// Request mocks the Request function of HTTPContext
+func (c *MockedHTTPContext) Request() context.HTTPRequest {
+	return &c.MockedRequest
+}
+
+// Response mocks the Response function of HTTPContext
+func (c *MockedHTTPContext) Response() context.HTTPResponse {
+	return &c.MockedResponse
+}
+
+// Deadline mocks the Deadline function of HTTPContext
+func (c *MockedHTTPContext) Deadline() (deadline time.Time, ok bool) {
+	if c.MockedDeadline != nil {
+		return c.MockedDeadline()
+	}
+	return time.Now(), false
+}
+
+// Done mocks the Done function of HTTPContext
+func (c *MockedHTTPContext) Done() <-chan struct{} {
+	if c.MockedDone != nil {
+		return c.MockedDone()
+	}
+	return nil
+}
+
+// Err mocks the Err function of HTTPContext
+func (c *MockedHTTPContext) Err() error {
+	if c.MockedErr != nil {
+		return c.MockedErr()
+	}
+	return nil
+}
+
+// Value mocks the Value function of HTTPContext
+func (c *MockedHTTPContext) Value(key interface{}) interface{} {
+	if c.MockedValue != nil {
+		return c.MockedValue(key)
+	}
+	return nil
+}
+
+// Cancel mocks the Cancel function of HTTPContext
+func (c *MockedHTTPContext) Cancel(err error) {
+	if c.MockedCancel != nil {
+		c.MockedCancel(err)
+	}
+}
+
+// Cancelled mocks the Cancelled function of HTTPContext
+func (c *MockedHTTPContext) Cancelled() bool {
+	if c.MockedCancelled != nil {
+		return c.MockedCancelled()
+	}
+	return false
+}
+
+// ClientDisconnected mocks the ClientDisconnected function of HTTPContext
+func (c *MockedHTTPContext) ClientDisconnected() bool {
+	if c.MockedClientDisconnected != nil {
+		return c.MockedClientDisconnected()
+	}
+	return false
+}
+
+// Duration mocks the Duration function of HTTPContext
+func (c *MockedHTTPContext) Duration() time.Duration {
+	if c.MockedDuration != nil {
+		return c.MockedDuration()
+	}
+	return 0
+}
+
+// OnFinish mocks the OnFinish function of HTTPContext
+func (c *MockedHTTPContext) OnFinish(fn func()) {
+	if c.MockedFinish != nil {
+		c.MockedOnFinish(fn)
+	}
+	c.lock.Lock()
+	c.finishFuncs = append(c.finishFuncs, fn)
+	c.lock.Unlock()
+}
+
+// AddTag mocks the AddTag function of HTTPContext
+func (c *MockedHTTPContext) AddTag(tag string) {
+	if c.MockedAddTag != nil {
+		c.MockedAddTag(tag)
+	}
+}
+
+// StatMetric mocks the StatMetric function of HTTPContext
+func (c *MockedHTTPContext) StatMetric() *httpstat.Metric {
+	if c.MockedStatMetric != nil {
+		return c.MockedStatMetric()
+	}
+	return nil
+}
+
+// Log mocks the Log function of HTTPContext
+func (c *MockedHTTPContext) Log() string {
+	if c.MockedLog != nil {
+		return c.MockedLog()
+	}
+	return ""
+}
+
+// Finish mocks the Finish function of HTTPContext
+func (c *MockedHTTPContext) Finish() {
+	if c.MockedFinish != nil {
+		c.MockedFinish()
+		return
+	}
+
+	c.lock.Lock()
+	for _, fn := range c.finishFuncs {
+		func() {
+			defer func() {
+				if err := recover(); err != nil {
+				}
+			}()
+			fn()
+		}()
+	}
+	c.lock.Unlock()
+}
+
+// Template mocks the Template function of HTTPContext
+func (c *MockedHTTPContext) Template() texttemplate.TemplateEngine {
+	if c.MockedTemplate != nil {
+		return c.MockedTemplate()
+	}
+	return nil
+}
+
+// SetTemplate mocks the SetTemplate function of HTTPContext
+func (c *MockedHTTPContext) SetTemplate(ht *context.HTTPTemplate) {
+	if c.MockedSetTemplate != nil {
+		c.MockedSetTemplate(ht)
+	}
+}
+
+// SaveReqToTemplate mocks the SaveReqToTemplate function of HTTPContext
+func (c *MockedHTTPContext) SaveReqToTemplate(filterName string) error {
+	if c.MockedSaveReqToTemplate != nil {
+		return c.MockedSaveReqToTemplate(filterName)
+	}
+	return nil
+}
+
+// SaveRspToTemplate mocks the SaveRspToTemplate function of HTTPContext
+func (c *MockedHTTPContext) SaveRspToTemplate(filterName string) error {
+	if c.MockedSaveRspToTemplate != nil {
+		return c.MockedSaveRspToTemplate(filterName)
+	}
+	return nil
+}
+
+// CallNextHandler mocks the CallNextHandler function of HTTPContext
+func (c *MockedHTTPContext) CallNextHandler(lastResult string) string {
+	if c.MockedCallNextHandler != nil {
+		return c.MockedCallNextHandler(lastResult)
+	}
+	return lastResult
+}
+
+// SetHandlerCaller mocks the SetHandlerCaller function of HTTPContext
+func (c *MockedHTTPContext) SetHandlerCaller(caller context.HandlerCaller) {
+	if c.MockedSetHandlerCaller != nil {
+		c.SetHandlerCaller(caller)
+	}
+}
diff --git a/pkg/context/contexttest/request.go b/pkg/context/contexttest/request.go
new file mode 100644
index 00000000..aac1a914
--- /dev/null
+++ b/pkg/context/contexttest/request.go
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package contexttest
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/megaease/easegress/pkg/util/httpheader"
+)
+
+// MockedHTTPRequest is the mocked HTTP request
+type MockedHTTPRequest struct {
+	MockedRealIP      func() string
+	MockedMethod      func() string
+	MockedSetMethod   func(method string)
+	MockedScheme      func() string
+	MockedHost        func() string
+	MockedSetHost     func(host string)
+	MockedPath        func() string
+	MockedSetPath     func(path string)
+	MockedEscapedPath func() string
+	MockedQuery       func() string
+	MockedSetQuery    func(query string)
+	MockedFragment    func() string
+	MockedProto       func() string
+	MockedHeader      func() *httpheader.HTTPHeader
+	MockedCookie      func(name string) (*http.Cookie, error)
+	MockedCookies     func() []*http.Cookie
+	MockedAddCookie   func(cookie *http.Cookie)
+	MockedBody        func() io.Reader
+	MockedSetBody     func(io.Reader)
+	MockedStd         func() *http.Request
+	MockedSize        func() uint64
+}
+
+// RealIP mocks the RealIP function of HTTPRequest
+func (r *MockedHTTPRequest) RealIP() string {
+	if r.MockedRealIP != nil {
+		return r.MockedRealIP()
+	}
+	return ""
+}
+
+// Method mocks the Method function of HTTPRequest
+func (r *MockedHTTPRequest) Method() string {
+	if r.MockedMethod != nil {
+		return r.MockedMethod()
+	}
+	return ""
+}
+
+// SetMethod mocks the SetMethod function of HTTPRequest
+func (r *MockedHTTPRequest) SetMethod(method string) {
+	if r.MockedSetMethod != nil {
+		r.MockedSetMethod(method)
+	}
+}
+
+// Scheme mocks the Scheme function of HTTPRequest
+func (r *MockedHTTPRequest) Scheme() string {
+	if r.MockedScheme != nil {
+		return r.MockedScheme()
+	}
+	return ""
+}
+
+// Host mocks the Host function of HTTPRequest
+func (r *MockedHTTPRequest) Host() string {
+	if r.MockedHost != nil {
+		return r.MockedHost()
+	}
+	return ""
+}
+
+// SetHost mocks the SetHost function of HTTPRequest
+func (r *MockedHTTPRequest) SetHost(host string) {
+	if r.MockedSetHost != nil {
+		r.MockedSetHost(host)
+	}
+}
+
+// Path mocks the Path function of HTTPRequest
+func (r *MockedHTTPRequest) Path() string {
+	if r.MockedPath != nil {
+		return r.MockedPath()
+	}
+	return ""
+}
+
+// SetPath mocks the SetPath function of HTTPRequest
+func (r *MockedHTTPRequest) SetPath(path string) {
+	if r.MockedSetPath != nil {
+		r.MockedSetPath(path)
+	}
+}
+
+// EscapedPath mocks the EscapedPath function of HTTPRequest
+func (r *MockedHTTPRequest) EscapedPath() string {
+	if r.MockedEscapedPath != nil {
+		return r.MockedEscapedPath()
+	}
+	return ""
+}
+
+// Query mocks the Query function of HTTPRequest
+func (r *MockedHTTPRequest) Query() string {
+	if r.MockedQuery != nil {
+		return r.MockedQuery()
+	}
+	return ""
+}
+
+// SetQuery mocks the SetQuery function of HTTPRequest
+func (r *MockedHTTPRequest) SetQuery(query string) {
+	if r.MockedSetQuery != nil {
+		r.MockedSetQuery(query)
+	}
+}
+
+// Fragment mocks the Fragment function of HTTPRequest
+func (r *MockedHTTPRequest) Fragment() string {
+	if r.MockedFragment != nil {
+		return r.MockedFragment()
+	}
+	return ""
+}
+
+// Proto mocks the Proto function of HTTPRequest
+func (r *MockedHTTPRequest) Proto() string {
+	if r.MockedProto != nil {
+		return r.MockedProto()
+	}
+	return ""
+}
+
+// Header mocks the Header function of HTTPRequest
+func (r *MockedHTTPRequest) Header() *httpheader.HTTPHeader {
+	if r.MockedHeader != nil {
+		return r.MockedHeader()
+	}
+	return nil
+}
+
+// Cookie mocks the Cookie function of HTTPRequest
+func (r *MockedHTTPRequest) Cookie(name string) (*http.Cookie, error) {
+	if r.MockedCookie != nil {
+		return r.MockedCookie(name)
+	}
+	return nil, nil
+}
+
+// Cookies mocks the Cookies function of HTTPRequest
+func (r *MockedHTTPRequest) Cookies() []*http.Cookie {
+	if r.MockedCookies != nil {
+		return r.MockedCookies()
+	}
+	return nil
+}
+
+// AddCookie mocks the AddCookie function of HTTPRequest
+func (r *MockedHTTPRequest) AddCookie(cookie *http.Cookie) {
+	if r.MockedAddCookie != nil {
+		r.MockedAddCookie(cookie)
+	}
+}
+
+// Body mocks the Body function of HTTPRequest
+func (r *MockedHTTPRequest) Body() io.Reader {
+	if r.MockedBody != nil {
+		return r.MockedBody()
+	}
+	return nil
+}
+
+// SetBody mocks the SetBody function of HTTPRequest
+func (r *MockedHTTPRequest) SetBody(body io.Reader) {
+	if r.MockedSetBody != nil {
+		r.MockedSetBody(body)
+	}
+}
+
+// Std mocks the Std function of HTTPRequest
+func (r *MockedHTTPRequest) Std() *http.Request {
+	if r.MockedStd != nil {
+		return r.MockedStd()
+	}
+	return &http.Request{}
+}
+
+// Size mocks the Size function of HTTPRequest
+func (r *MockedHTTPRequest) Size() uint64 {
+	if r.MockedSize != nil {
+		return r.MockedSize()
+	}
+	return 0
+}
diff --git a/pkg/context/contexttest/response.go b/pkg/context/contexttest/response.go
new file mode 100644
index 00000000..cd2d6a50
--- /dev/null
+++ b/pkg/context/contexttest/response.go
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package contexttest
+
+import (
+	"io"
+	"net/http"
+	"net/http/httptest"
+
+	"github.com/megaease/easegress/pkg/util/httpheader"
+)
+
+// MockedHTTPResponse is the mocked HTTP response
+type MockedHTTPResponse struct {
+	MockedStatusCode    func() int
+	MockedSetStatusCode func(code int)
+	MockedHeader        func() *httpheader.HTTPHeader
+	MockedSetCookie     func(cookie *http.Cookie)
+	MockedSetBody       func(body io.Reader)
+	MockedBody          func() io.Reader
+	MockedOnFlushBody   func(func(body []byte, complete bool) (newBody []byte))
+	MockedStd           func() http.ResponseWriter
+	MockedSize          func() uint64
+}
+
+// StatusCode returns the status code
+func (r *MockedHTTPResponse) StatusCode() int {
+	if r.MockedStatusCode != nil {
+		return r.MockedStatusCode()
+	}
+	return 0
+}
+
+// SetStatusCode set the status code
+func (r *MockedHTTPResponse) SetStatusCode(code int) {
+	if r.MockedSetStatusCode != nil {
+		r.MockedSetStatusCode(code)
+	}
+}
+
+// Header returns the header
+func (r *MockedHTTPResponse) Header() *httpheader.HTTPHeader {
+	if r.MockedHeader != nil {
+		return r.MockedHeader()
+	}
+	return nil
+}
+
+// SetCookie sets a cookie
+func (r *MockedHTTPResponse) SetCookie(cookie *http.Cookie) {
+	if r.MockedSetCookie != nil {
+		r.MockedSetCookie(cookie)
+	}
+}
+
+// SetBody sets the response body
+func (r *MockedHTTPResponse) SetBody(body io.Reader) {
+	if r.MockedSetBody != nil {
+		r.MockedSetBody(body)
+	}
+}
+
+// Body returns the response body
+func (r *MockedHTTPResponse) Body() io.Reader {
+	if r.MockedBody != nil {
+		return r.MockedBody()
+	}
+	return nil
+}
+
+// OnFlushBody registers a callback function on flush body
+func (r *MockedHTTPResponse) OnFlushBody(fn func(body []byte, complete bool) (newBody []byte)) {
+	if r.MockedOnFlushBody != nil {
+		r.MockedOnFlushBody(fn)
+	}
+}
+
+// Std returns the stardard response
+func (r *MockedHTTPResponse) Std() http.ResponseWriter {
+	if r.MockedStd != nil {
+		return r.MockedStd()
+	}
+	return &httptest.ResponseRecorder{}
+}
+
+// Size returns the response size
+func (r *MockedHTTPResponse) Size() uint64 {
+	if r.MockedSize != nil {
+		return r.MockedSize()
+	}
+	return 0
+}
diff --git a/pkg/filter/canary/matcher_test.go b/pkg/filter/canary/matcher_test.go
index 29a59c0e..e04fe080 100644
--- a/pkg/filter/canary/matcher_test.go
+++ b/pkg/filter/canary/matcher_test.go
@@ -20,28 +20,14 @@ package canary
 import (
 	"net/http"
 	"os"
-	"path/filepath"
 	"testing"
 
 	"github.com/megaease/easegress/pkg/logger"
-	"github.com/megaease/easegress/pkg/option"
 )
 
-const tempDir = "/tmp/eg-test"
-
 func TestMain(m *testing.M) {
-	absLogDir := filepath.Join(tempDir, "global-log")
-	os.MkdirAll(absLogDir, 0o755)
-	logger.Init(&option.Options{
-		Name:      "canary-for-log",
-		AbsLogDir: absLogDir,
-	})
-
+	logger.InitNop()
 	code := m.Run()
-
-	logger.Sync()
-	os.RemoveAll(tempDir)
-
 	os.Exit(code)
 }
 
diff --git a/pkg/filter/circuitbreaker/circuitbreaker_test.go b/pkg/filter/circuitbreaker/circuitbreaker_test.go
new file mode 100644
index 00000000..94792ffc
--- /dev/null
+++ b/pkg/filter/circuitbreaker/circuitbreaker_test.go
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package circuitbreaker
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/logger"
+	"github.com/megaease/easegress/pkg/object/httppipeline"
+	libcb "github.com/megaease/easegress/pkg/util/circuitbreaker"
+	"github.com/megaease/easegress/pkg/util/yamltool"
+)
+
+func TestMain(m *testing.M) {
+	logger.InitNop()
+	code := m.Run()
+	os.Exit(code)
+}
+
+func TestCircuitBreaker(t *testing.T) {
+	const yamlSpec = `
+kind: CircuitBreaker
+name: circuitbreaker
+policies:
+- name: default
+  slowCallRateThreshold: 1
+  failureRateThreshold: 50
+  slidingWindowType: COUNT_BASED
+  slidingWindowSize: 10
+  minimumNumberOfCalls: 5
+  failureStatusCodes: [500, 503]
+defaultPolicyRef: default
+urls:
+- methods: []
+  url:
+    exact: /circuitbreak
+    prefix:
+    regex:
+`
+	rawSpec := make(map[string]interface{})
+	yamltool.Unmarshal([]byte(yamlSpec), &rawSpec)
+
+	spec, e := httppipeline.NewFilterSpec(rawSpec, nil)
+	if e != nil {
+		t.Errorf("unexpected error: %v", e)
+	}
+
+	cb := &CircuitBreaker{}
+	cb.Init(spec)
+
+	resp := httptest.NewRecorder()
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedMethod = func() string {
+		return http.MethodGet
+	}
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/circuitbreak"
+	}
+	ctx.MockedResponse.MockedStd = func() http.ResponseWriter {
+		return resp
+	}
+	ctx.MockedResponse.MockedStatusCode = func() int {
+		return http.StatusInternalServerError
+	}
+
+	for i := 0; i < 5; i++ {
+		result := cb.Handle(ctx)
+		if result == resultShortCircuited {
+			t.Error("should not be short circuited")
+		}
+	}
+
+	result := cb.Handle(ctx)
+	if result != resultShortCircuited {
+		t.Error("should be short circuited")
+	}
+
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/notcircuitbreak"
+	}
+	result = cb.Handle(ctx)
+	if result == resultShortCircuited {
+		t.Error("should not be short circuited")
+	}
+
+	if cb.Status() != nil {
+		t.Error("behavior changed, please update this case")
+	}
+	cb.Description()
+
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/circuitbreak"
+	}
+	newCb := &CircuitBreaker{}
+	spec, _ = httppipeline.NewFilterSpec(rawSpec, nil)
+	newCb.Inherit(spec, cb)
+	cb.Close()
+	result = newCb.Handle(ctx)
+	if result != resultShortCircuited {
+		t.Error("new circuit breaker should be short circuited")
+	}
+}
+
+func TestBuildPolicy(t *testing.T) {
+	url := &URLRule{
+		policy: &Policy{
+			SlidingWindowType:         "time_based",
+			SlowCallDurationThreshold: "10ms",
+			MaxWaitDurationInHalfOpen: "30s",
+			WaitDurationInOpen:        "60s",
+		},
+	}
+	p := url.buildPolicy()
+	if p.FailureRateThreshold != 50 {
+		t.Error("failure rate threshold is not default")
+	}
+	if p.SlowCallRateThreshold != 100 {
+		t.Error("slow call rate threshold is not default")
+	}
+	if p.SlidingWindowType != libcb.TimeBased {
+		t.Error("sliding window type is incorrect")
+	}
+	if p.SlidingWindowSize != 100 {
+		t.Error("sliding window size is not default")
+	}
+	if p.PermittedNumberOfCallsInHalfOpen != 10 {
+		t.Error("permitted number of calls in half open is not default")
+	}
+	if p.MinimumNumberOfCalls != 100 {
+		t.Error("minimum number of calls is not default")
+	}
+	if p.SlowCallDurationThreshold != 10*time.Millisecond {
+		t.Error("slow call duration threshold is not 10ms")
+	}
+	if p.MaxWaitDurationInHalfOpen != 30*time.Second {
+		t.Error("max wait duration in half open is not 30s")
+	}
+	if p.WaitDurationInOpen != time.Minute {
+		t.Error("wait duration in open is not 1m")
+	}
+}
diff --git a/pkg/filter/corsadaptor/corsadaptor_test.go b/pkg/filter/corsadaptor/corsadaptor_test.go
new file mode 100644
index 00000000..3790363d
--- /dev/null
+++ b/pkg/filter/corsadaptor/corsadaptor_test.go
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package corsadaptor
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/object/httppipeline"
+	"github.com/megaease/easegress/pkg/util/httpheader"
+	"github.com/megaease/easegress/pkg/util/yamltool"
+)
+
+func TestCORSAdaptor(t *testing.T) {
+	const yamlSpec = `
+kind: CORSAdaptor
+name: cors
+`
+	rawSpec := make(map[string]interface{})
+	yamltool.Unmarshal([]byte(yamlSpec), &rawSpec)
+
+	spec, e := httppipeline.NewFilterSpec(rawSpec, nil)
+	if e != nil {
+		t.Errorf("unexpected error: %v", e)
+	}
+
+	cors := &CORSAdaptor{}
+	cors.Init(spec)
+
+	header := http.Header{}
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedMethod = func() string {
+		return http.MethodOptions
+	}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+
+	result := cors.Handle(ctx)
+	if result == resultPreflighted {
+		t.Error("request should not be preflighted")
+	}
+
+	header.Add("Access-Control-Request-Method", "abc")
+	result = cors.Handle(ctx)
+	if result != resultPreflighted {
+		t.Error("request should be preflighted")
+	}
+
+	newCors := &CORSAdaptor{}
+	spec, _ = httppipeline.NewFilterSpec(rawSpec, nil)
+	newCors.Inherit(spec, cors)
+	cors.Close()
+	ctx.MockedRequest.MockedMethod = func() string {
+		return http.MethodGet
+	}
+	result = newCors.Handle(ctx)
+	if result == resultPreflighted {
+		t.Error("request should not be preflighted")
+	}
+}
diff --git a/pkg/filter/fallback/fallback_test.go b/pkg/filter/fallback/fallback_test.go
new file mode 100644
index 00000000..03aa1da2
--- /dev/null
+++ b/pkg/filter/fallback/fallback_test.go
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fallback
+
+import (
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/object/httppipeline"
+	"github.com/megaease/easegress/pkg/util/httpheader"
+	"github.com/megaease/easegress/pkg/util/yamltool"
+)
+
+func TestFallback(t *testing.T) {
+	const yamlSpec = `
+kind: Fallback
+name: fallback
+mockCode: 203
+mockHeaders:
+  X-Mocked: true
+mockBody: "mocked body"
+`
+	rawSpec := make(map[string]interface{})
+	yamltool.Unmarshal([]byte(yamlSpec), &rawSpec)
+
+	spec, e := httppipeline.NewFilterSpec(rawSpec, nil)
+	if e != nil {
+		t.Errorf("unexpected error: %v", e)
+	}
+
+	fb := &Fallback{}
+	fb.Init(spec)
+
+	resp := httptest.NewRecorder()
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedResponse.MockedSetStatusCode = func(code int) {
+		resp.WriteHeader(code)
+	}
+	ctx.MockedResponse.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(resp.Header())
+	}
+	ctx.MockedResponse.MockedSetBody = func(body io.Reader) {
+		data, _ := io.ReadAll(body)
+		resp.Write(data)
+	}
+
+	fb.Handle(ctx)
+	if resp.Code != 203 {
+		t.Error("status code is not correct")
+	}
+	if resp.Body.String() != "mocked body" {
+		t.Error("body is not correct")
+	}
+	if resp.Header().Get("X-Mocked") != "true" {
+		t.Error("header is not correct")
+	}
+
+	if fb.Status() != nil {
+		t.Error("behavior changed, please update this case")
+	}
+	fb.Description()
+
+	newFb := &Fallback{}
+	spec, _ = httppipeline.NewFilterSpec(rawSpec, nil)
+	newFb.Inherit(spec, fb)
+	fb.Close()
+	ctx.MockedRequest.MockedMethod = func() string {
+		return http.MethodGet
+	}
+
+	resp = httptest.NewRecorder()
+	newFb.Handle(ctx)
+	if resp.Code != 203 {
+		t.Error("status code is not correct")
+	}
+	if resp.Body.String() != "mocked body" {
+		t.Error("body is not correct")
+	}
+	if resp.Header().Get("X-Mocked") != "true" {
+		t.Error("header is not correct")
+	}
+}
diff --git a/pkg/filter/mock/mock_test.go b/pkg/filter/mock/mock_test.go
new file mode 100644
index 00000000..afac3f2a
--- /dev/null
+++ b/pkg/filter/mock/mock_test.go
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mock
+
+import (
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/logger"
+	"github.com/megaease/easegress/pkg/object/httppipeline"
+	"github.com/megaease/easegress/pkg/util/httpheader"
+	"github.com/megaease/easegress/pkg/util/yamltool"
+)
+
+func TestMain(m *testing.M) {
+	logger.InitNop()
+	code := m.Run()
+	os.Exit(code)
+}
+
+func TestMock(t *testing.T) {
+	const yamlSpec = `
+kind: Mock
+name: mock
+rules:
+- pathPrefix: /login/
+  code: 202
+  body: 'mocked body'
+  headers:
+    X-Test: test1
+- path: /sales
+  code: 203
+  body: 'mocked body'
+  headers:
+    X-Test: test2
+  delay: 1ms
+- code: 204
+  body: 'mocked body 2'
+  headers:
+    X-Test: test3
+`
+	rawSpec := make(map[string]interface{})
+	yamltool.Unmarshal([]byte(yamlSpec), &rawSpec)
+
+	spec, e := httppipeline.NewFilterSpec(rawSpec, nil)
+	if e != nil {
+		t.Errorf("unexpected error: %v", e)
+	}
+
+	m := &Mock{}
+	m.Init(spec)
+
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedMethod = func() string {
+		return http.MethodGet
+	}
+	resp := httptest.NewRecorder()
+	ctx.MockedResponse.MockedSetStatusCode = func(code int) {
+		resp.WriteHeader(code)
+	}
+	ctx.MockedResponse.MockedSetBody = func(body io.Reader) {
+		data, _ := io.ReadAll(body)
+		resp.Write(data)
+	}
+	ctx.MockedResponse.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(resp.Header())
+	}
+	ctx.MockedCallNextHandler = func(lastResult string) string {
+		return ""
+	}
+
+	resp = httptest.NewRecorder()
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/login/1"
+	}
+	m.Handle(ctx)
+	if resp.Code != 202 {
+		t.Error("status code is not 202")
+	}
+
+	resp = httptest.NewRecorder()
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/sales"
+	}
+	m.Handle(ctx)
+	if resp.Code != 203 {
+		t.Error("status code is not 203")
+	}
+
+	if resp.Body.String() != "mocked body" {
+		t.Error("body should be 'mocked body'")
+	}
+
+	if resp.Header().Get("X-Test") != "test2" {
+		t.Error("header 'X-Test' should be 'test2'")
+	}
+
+	if m.Status() != nil {
+		t.Error("behavior changed, please update this case")
+	}
+	m.Description()
+
+	resp = httptest.NewRecorder()
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/customer"
+	}
+	newM := &Mock{}
+	spec, _ = httppipeline.NewFilterSpec(rawSpec, nil)
+	newM.Inherit(spec, m)
+	m.Close()
+	newM.Handle(ctx)
+	if resp.Code != 204 {
+		t.Error("status code is not 204")
+	}
+}
diff --git a/pkg/filter/proxy/compression_test.go b/pkg/filter/proxy/compression_test.go
new file mode 100644
index 00000000..f4efbbc4
--- /dev/null
+++ b/pkg/filter/proxy/compression_test.go
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package proxy
+
+import (
+	"io"
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/logger"
+	"github.com/megaease/easegress/pkg/util/httpheader"
+)
+
+func TestMain(m *testing.M) {
+	logger.InitNop()
+	code := m.Run()
+	os.Exit(code)
+}
+
+func TestAcceptGzip(t *testing.T) {
+	c := newCompression(&CompressionSpec{MinLength: 100})
+
+	header := http.Header{}
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+
+	if !c.acceptGzip(ctx) {
+		t.Error("accept gzip should be true")
+	}
+
+	header.Add(httpheader.KeyAcceptEncoding, "text/text")
+	if c.acceptGzip(ctx) {
+		t.Error("accept gzip should be false")
+	}
+
+	header.Add(httpheader.KeyAcceptEncoding, "*/*")
+	if !c.acceptGzip(ctx) {
+		t.Error("accept gzip should be true")
+	}
+
+	header.Del(httpheader.KeyAcceptEncoding)
+	header.Add(httpheader.KeyAcceptEncoding, "gzip")
+	if !c.acceptGzip(ctx) {
+		t.Error("accept gzip should be true")
+	}
+}
+
+func TestAlreadyGziped(t *testing.T) {
+	c := newCompression(&CompressionSpec{MinLength: 100})
+
+	header := http.Header{}
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedResponse.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+
+	if c.alreadyGziped(ctx) {
+		t.Error("already gziped should be false")
+	}
+
+	header.Add(httpheader.KeyContentEncoding, "text")
+	if c.alreadyGziped(ctx) {
+		t.Error("already gziped should be false")
+	}
+
+	header.Add(httpheader.KeyContentEncoding, "gzip")
+	if !c.alreadyGziped(ctx) {
+		t.Error("already gziped should be true")
+	}
+}
+
+func TestParseContentLength(t *testing.T) {
+	c := newCompression(&CompressionSpec{MinLength: 100})
+
+	header := http.Header{}
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedResponse.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+
+	if c.parseContentLength(ctx) != -1 {
+		t.Error("content length should be -1")
+	}
+
+	header.Set(httpheader.KeyContentLength, "abc")
+	if c.parseContentLength(ctx) != -1 {
+		t.Error("content length should be -1")
+	}
+
+	header.Set(httpheader.KeyContentLength, "100")
+	if c.parseContentLength(ctx) != 100 {
+		t.Error("content length should be 100")
+	}
+}
+
+func TestCompress(t *testing.T) {
+	c := newCompression(&CompressionSpec{MinLength: 100})
+
+	header := http.Header{}
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+	ctx.MockedResponse.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+	header.Set(httpheader.KeyContentLength, "20")
+
+	rawBody := strings.Repeat("this is the raw body. ", 100)
+	sr := strings.NewReader(rawBody)
+	ctx.MockedResponse.MockedBody = func() io.Reader {
+		return sr
+	}
+
+	c.compress(ctx)
+	if header.Get(httpheader.KeyContentEncoding) == "gzip" {
+		t.Error("body should not be gziped")
+	}
+
+	ctx.MockedResponse.MockedSetBody = func(body io.Reader) {
+		io.ReadAll(body)
+	}
+
+	header.Set(httpheader.KeyContentLength, "120")
+	c.compress(ctx)
+	if header.Get(httpheader.KeyContentEncoding) != "gzip" {
+		t.Error("body should be gziped")
+	}
+
+}
diff --git a/pkg/filter/proxy/masterslavereader_test.go b/pkg/filter/proxy/masterslavereader_test.go
new file mode 100644
index 00000000..d8af3061
--- /dev/null
+++ b/pkg/filter/proxy/masterslavereader_test.go
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package proxy
+
+import (
+	"bytes"
+	"io"
+	"testing"
+	"testing/iotest"
+)
+
+func TestReader(t *testing.T) {
+	var b bytes.Buffer
+	// Write strings to the Buffer.
+	b.WriteString("ABC")
+
+	r := bytes.NewReader(b.Bytes())
+
+	reader1, reader2 := newMasterSlaveReader(r)
+
+	buff := make([]byte, 10)
+	n, _ := reader1.Read(buff)
+
+	if n == 0 {
+		t.Errorf("master read failed")
+	}
+
+	buff1 := make([]byte, 10)
+	n, _ = reader2.Read(buff1)
+	if n == 0 {
+		t.Errorf("slave read failed")
+	}
+
+	if string(buff) != string(buff1) {
+		t.Errorf("buff: %s, buff1: %s not the same ", string(buff), string(buff1))
+	}
+
+	reader1.Close()
+}
+
+func TestEOFReader(t *testing.T) {
+	eofReader := iotest.ErrReader(io.EOF)
+
+	reader1, reader2 := newMasterSlaveReader(eofReader)
+
+	buff := make([]byte, 10)
+	_, err := reader1.Read(buff)
+
+	if err == nil {
+		t.Errorf("master read should failed, but succ")
+	}
+
+	buff1 := make([]byte, 10)
+	_, err = reader2.Read(buff1)
+	if err == nil {
+		t.Errorf("slave read should failed but succ ")
+	}
+
+	if string(buff) != string(buff1) {
+		t.Errorf("buff: %s, buff1: %s not the same ", string(buff), string(buff1))
+	}
+
+	reader1.Close()
+}
diff --git a/pkg/filter/proxy/proxy_test.go b/pkg/filter/proxy/proxy_test.go
new file mode 100644
index 00000000..987e12e3
--- /dev/null
+++ b/pkg/filter/proxy/proxy_test.go
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package proxy
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/object/httppipeline"
+	"github.com/megaease/easegress/pkg/util/httpfilter"
+	"github.com/megaease/easegress/pkg/util/httpheader"
+	"github.com/megaease/easegress/pkg/util/memorycache"
+	"github.com/megaease/easegress/pkg/util/yamltool"
+)
+
+func TestProxy(t *testing.T) {
+	const yamlSpec = `
+name: proxy
+kind: Proxy
+fallback:
+  forCodes: true
+  mockCode: 200
+  mockHeaders:
+    X-Mock: mocked
+  mockBody: this is the mocked body
+mainPool:
+  servers:
+  - url: http://127.0.0.1:9095
+  - url: http://127.0.0.1:9096
+  - url: http://127.0.0.1:9097
+  loadBalance:
+    policy: roundRobin
+candidatePools:
+- filter:
+    headers:
+      "X-Test":
+        exact: testheader
+  servers:
+  - url: http://127.0.0.2:9095
+  - url: http://127.0.0.2:9096
+  - url: http://127.0.0.2:9097
+  - url: http://127.0.0.2:9098
+  loadBalance:
+    policy: roundRobin
+mirrorPool:
+  filter:
+    headers:
+      "X-Mirror":
+        exact: mirror
+  servers:
+  - url: http://127.0.0.3:9095
+  - url: http://127.0.0.3:9096
+  loadBalance:
+    policy: roundRobin
+compression:
+  minLength: 1024
+failureCodes: [503, 504]
+`
+	rawSpec := make(map[string]interface{})
+	yamltool.Unmarshal([]byte(yamlSpec), &rawSpec)
+
+	spec, e := httppipeline.NewFilterSpec(rawSpec, nil)
+	if e != nil {
+		t.Errorf("unexpected error: %v", e)
+	}
+
+	proxy := &Proxy{}
+	proxy.Init(spec)
+
+	if len(proxy.candidatePools) != 1 {
+		t.Error("length of candidate pools is incorrect")
+	}
+	if len(proxy.mirrorPool.spec.Servers) != 2 {
+		t.Error("server count of mirror pool is incorrect")
+	}
+
+	status := proxy.Status()
+	if status == nil {
+		t.Error("status should not be nil")
+	}
+
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedResponse.MockedStatusCode = func() int {
+		return http.StatusServiceUnavailable
+	}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		header := http.Header{}
+		header.Set("X-Mirror", "mirror")
+		return httpheader.New(header)
+	}
+	ctx.MockedResponse.MockedHeader = func() *httpheader.HTTPHeader {
+		header := http.Header{}
+		return httpheader.New(header)
+	}
+	if !proxy.fallbackForCodes(ctx) {
+		t.Error("fallback for 503 should be true")
+	}
+	ctx.MockedResponse.MockedStatusCode = func() int {
+		return http.StatusInternalServerError
+	}
+	if proxy.fallbackForCodes(ctx) {
+		t.Error("fallback for 500 should be false")
+	}
+
+	fnSendRequest = func(r *http.Request) (*http.Response, error) {
+		return &http.Response{
+			Body: io.NopCloser(strings.NewReader("this is the body")),
+		}, nil
+	}
+	result := proxy.Handle(ctx)
+	if result != "" {
+		t.Error("proxy.Handle should succeeded")
+	}
+	ctx.Finish()
+
+	fnSendRequest = func(r *http.Request) (*http.Response, error) {
+		return nil, fmt.Errorf("mocked error")
+	}
+	result = proxy.Handle(ctx)
+	if result == "" {
+		t.Error("proxy.Handle should fail")
+	}
+
+	time.Sleep(10 * time.Millisecond)
+	proxy.Close()
+	time.Sleep(10 * time.Millisecond)
+}
+
+func TestSpecValidate(t *testing.T) {
+	spec := Spec{}
+
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+
+	spec.MainPool = &PoolSpec{}
+	spec.MainPool.Filter = &httpfilter.Spec{}
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+
+	spec.MainPool.Filter = nil
+	if spec.Validate() != nil {
+		t.Error("validate should succeed")
+	}
+
+	spec.CandidatePools = append(spec.CandidatePools, &PoolSpec{})
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+
+	spec.CandidatePools[0].Filter = &httpfilter.Spec{}
+	if spec.Validate() != nil {
+		t.Error("validate should succeed")
+	}
+
+	spec.MirrorPool = &PoolSpec{}
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+
+	spec.MirrorPool.Filter = &httpfilter.Spec{}
+	if spec.Validate() != nil {
+		t.Error("validate should succeed")
+	}
+
+	spec.MirrorPool.MemoryCache = &memorycache.Spec{}
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+	spec.MirrorPool.MemoryCache = nil
+
+	spec.Fallback = &FallbackSpec{}
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+
+	spec.FailureCodes = []int{500}
+	if spec.Validate() != nil {
+		t.Error("validate should succeed")
+	}
+}
+
+func TestPoolSpecValidate(t *testing.T) {
+	spec := PoolSpec{}
+
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+
+	servers := []*Server{
+		{
+			URL:  "http://127.0.0.1:9090",
+			Tags: []string{"d1", "v1", "green"},
+		},
+		{
+			URL:    "http://127.0.0.1:9091",
+			Tags:   []string{"v1", "d1", "green"},
+			Weight: 2,
+		},
+	}
+	spec.Servers = servers
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+
+	servers[0].Weight = 1
+	spec.ServersTags = []string{"v2"}
+	if spec.Validate() == nil {
+		t.Error("validate should fail")
+	}
+
+	spec.ServersTags = []string{"v1"}
+	if spec.Validate() != nil {
+		t.Error("validate should succeed")
+	}
+}
diff --git a/pkg/filter/proxy/request_test.go b/pkg/filter/proxy/request_test.go
new file mode 100644
index 00000000..69f4e2a4
--- /dev/null
+++ b/pkg/filter/proxy/request_test.go
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package proxy
+
+import (
+	"bytes"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/util/httpheader"
+)
+
+func TestRequest(t *testing.T) {
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/abc"
+	}
+	ctx.MockedRequest.MockedQuery = func() string {
+		return "state=1"
+	}
+	ctx.MockedRequest.MockedMethod = func() string {
+		return http.MethodGet
+	}
+	ctx.MockedRequest.MockedHost = func() string {
+		return "megaease.com"
+	}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(http.Header{})
+	}
+
+	server := Server{
+		URL: "http://192.168.1.2",
+	}
+
+	p := pool{}
+	sr := strings.NewReader("this is the raw body")
+	req, _ := p.newRequest(ctx, &server, sr)
+
+	req.start()
+	tm := req.startTime()
+
+	time.Sleep(time.Millisecond)
+	req.start()
+	if tm != req.startTime() {
+		t.Error("start time should not change")
+	}
+
+	req.total()
+
+	time.Sleep(time.Millisecond)
+	req.finish()
+	tm = req.endTime()
+	time.Sleep(time.Millisecond)
+	req.finish()
+
+	if tm != req.endTime() {
+		t.Error("end time should not change")
+	}
+
+	req.total()
+	if req.detail() == "" {
+		t.Error("detail should not be empty")
+	}
+}
+
+func TestResultState(t *testing.T) {
+	rs := &resultState{buff: &bytes.Buffer{}}
+	if n, b := rs.Width(); n != 0 || b {
+		t.Error("implementation changed, this case should be updated")
+	}
+	if n, b := rs.Precision(); n != 0 || b {
+		t.Error("implementation changed, this case should be updated")
+	}
+	if rs.Flag(1) {
+		t.Error("implementation changed, this case should be updated")
+	}
+}
diff --git a/pkg/filter/proxy/server_test.go b/pkg/filter/proxy/server_test.go
index 0a6e5ffe..7699caff 100644
--- a/pkg/filter/proxy/server_test.go
+++ b/pkg/filter/proxy/server_test.go
@@ -18,8 +18,16 @@
 package proxy
 
 import (
+	"fmt"
+	"net/http"
 	"reflect"
 	"testing"
+	"time"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/object/serviceregistry"
+	"github.com/megaease/easegress/pkg/util/hashtool"
+	"github.com/megaease/easegress/pkg/util/httpheader"
 )
 
 func TestPickservers(t *testing.T) {
@@ -201,3 +209,228 @@ func TestPickservers(t *testing.T) {
 		})
 	}
 }
+
+func TestStaticServers(t *testing.T) {
+	servers := []*Server{
+		{
+			URL:    "http://127.0.0.1:9090",
+			Tags:   []string{"d1", "v1", "green"},
+			Weight: 1,
+		},
+		{
+			URL:    "http://127.0.0.1:9091",
+			Tags:   []string{"v1", "d1", "green"},
+			Weight: 2,
+		},
+		{
+			URL:    "http://127.0.0.1:9092",
+			Tags:   []string{"green", "d1", "v1"},
+			Weight: 3,
+		},
+		{
+			URL:    "http://127.0.0.1:9093",
+			Tags:   []string{"v1"},
+			Weight: 4,
+		},
+		{
+			URL:    "http://127.0.0.1:9094",
+			Tags:   []string{"v1", "v3"},
+			Weight: 5,
+		},
+	}
+
+	for _, s := range servers {
+		str := s.String()
+		if str != fmt.Sprintf("%s,%v,%d", s.URL, s.Tags, s.Weight) {
+			t.Error("s.String returns unexpected result")
+		}
+	}
+
+	ss := newStaticServers(servers, []string{}, nil)
+	if ss.len() != len(servers) {
+		t.Errorf("ss.len() is not %d", len(servers))
+	}
+
+	ctx := &contexttest.MockedHTTPContext{}
+	for i := 0; i < len(servers); i++ {
+		if ss.next(ctx) != servers[i] {
+			t.Errorf("ss.next() returns unexpected server")
+		}
+	}
+
+	ss.lb.Policy = "badPolicy"
+	for i := 0; i < len(servers); i++ {
+		if ss.next(ctx) != servers[i] {
+			t.Errorf("ss.next() returns unexpected server")
+		}
+	}
+
+	ss.lb.Policy = PolicyRandom
+	for i := 0; i < len(servers); i++ {
+		ss.next(ctx)
+	}
+
+	ss.lb.Policy = PolicyWeightedRandom
+	for i := 0; i < len(servers); i++ {
+		ss.next(ctx)
+	}
+
+	ip := ""
+	ctx.MockedRequest.MockedRealIP = func() string {
+		return ip
+	}
+	ss.lb.Policy = PolicyIPHash
+	for i := 0; i < len(servers)*5; i++ {
+		ip = fmt.Sprintf("111.222.111.%d", i)
+		sum32 := int(hashtool.Hash32(ip))
+		s := servers[sum32%len(servers)]
+		if ss.next(ctx) != s {
+			t.Errorf("ss.next() returns unexpected server")
+		}
+	}
+
+	header := http.Header{}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+	ss.lb.Policy = PolicyHeaderHash
+	if ss.lb.Validate() == nil {
+		t.Error("LoadBalance.Validate should fail")
+	}
+
+	ss.lb.HeaderHashKey = "X-Megaease"
+	if ss.lb.Validate() != nil {
+		t.Error("LoadBalance.Validate should succeed")
+	}
+	for i := 0; i < len(servers)*5; i++ {
+		v := fmt.Sprintf("value-%d", i)
+		header.Set(ss.lb.HeaderHashKey, v)
+		sum32 := int(hashtool.Hash32(v))
+		s := servers[sum32%len(servers)]
+		if ss.next(ctx) != s {
+			t.Errorf("ss.next() returns unexpected server")
+		}
+	}
+}
+
+func TestServers(t *testing.T) {
+	servers := []*Server{
+		{
+			URL:    "http://127.0.0.1:9090",
+			Tags:   []string{"d1", "v1", "green"},
+			Weight: 1,
+		},
+		{
+			URL:    "http://127.0.0.1:9091",
+			Tags:   []string{"v1", "d1", "green"},
+			Weight: 2,
+		},
+		{
+			URL:    "http://127.0.0.1:9092",
+			Tags:   []string{"green", "d1", "v1"},
+			Weight: 3,
+		},
+		{
+			URL:    "http://127.0.0.1:9093",
+			Tags:   []string{"v1"},
+			Weight: 4,
+		},
+		{
+			URL:    "http://127.0.0.1:9094",
+			Tags:   []string{"v1", "v3"},
+			Weight: 5,
+		},
+	}
+	ps := &PoolSpec{
+		ServersTags:     []string{},
+		ServiceRegistry: "service registry",
+		ServiceName:     "service name",
+		Servers:         []*Server{},
+	}
+
+	ctx := &contexttest.MockedHTTPContext{}
+
+	s := newServers(ps)
+	if s.len() != 0 {
+		t.Errorf("servers.len() should be 0")
+	}
+	s.close()
+
+	ps = &PoolSpec{
+		ServersTags:     []string{},
+		ServiceRegistry: "service registry",
+		ServiceName:     "service name",
+		Servers:         servers,
+	}
+	s = newServers(ps)
+	if s.len() != len(servers) {
+		t.Errorf("servers.len() is not %d", len(servers))
+	}
+	for i := 0; i < len(servers); i++ {
+		if svr, e := s.next(ctx); e != nil || svr != servers[i] {
+			t.Errorf("ss.next() returns unexpected server")
+		}
+	}
+	s.close()
+
+	ps = &PoolSpec{
+		ServersTags:     []string{},
+		ServiceRegistry: "service registry",
+		Servers:         []*Server{},
+		ServiceName:     "testservice",
+	}
+	fnGetService.Store(func(serviceRegistry, serviceName string) (*serviceregistry.Service, error) {
+		return nil, fmt.Errorf("dummy error")
+	})
+	s = newServers(ps)
+	if s.len() != 0 {
+		t.Errorf("servers.len() should be 0")
+	}
+	s.close()
+
+	svcservers := []*serviceregistry.Server{
+		{
+			ServiceName: "testservice",
+			Hostname:    "server1",
+			HostIP:      "192.168.1.1",
+			Port:        80,
+		},
+		{
+			ServiceName: "testservice",
+			Hostname:    "server2",
+			HostIP:      "192.168.1.2",
+			Port:        80,
+		},
+		{
+			ServiceName: "testservice",
+			Hostname:    "server3",
+			HostIP:      "192.168.1.3",
+			Port:        80,
+		},
+	}
+	service, _ := serviceregistry.NewService("testservice", svcservers)
+
+	fnGetService.Store(func(serviceRegistry, serviceName string) (*serviceregistry.Service, error) {
+		return service, nil
+	})
+	s = newServers(ps)
+	if s.len() != len(service.Servers()) {
+		t.Errorf("servers.len() is not %d", len(service.Servers()))
+	}
+
+	svcservers = append(svcservers, &serviceregistry.Server{
+		ServiceName: "testservice",
+		Hostname:    "server4",
+		HostIP:      "192.168.1.4",
+		Port:        80,
+	})
+	service.Update(svcservers)
+
+	time.Sleep(100 * time.Millisecond)
+	if s.len() != len(service.Servers()) {
+		t.Errorf("servers.len() is not %d", len(service.Servers()))
+	}
+
+	service.Close("close")
+	s.close()
+}
diff --git a/pkg/filter/responseadaptor/responseadaptor_test.go b/pkg/filter/responseadaptor/responseadaptor_test.go
new file mode 100644
index 00000000..5a45885f
--- /dev/null
+++ b/pkg/filter/responseadaptor/responseadaptor_test.go
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package responseadaptor
+
+import (
+	"io"
+	"net/http/httptest"
+	"os"
+	"testing"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/logger"
+	"github.com/megaease/easegress/pkg/object/httppipeline"
+	"github.com/megaease/easegress/pkg/util/httpheader"
+	"github.com/megaease/easegress/pkg/util/texttemplate"
+	"github.com/megaease/easegress/pkg/util/yamltool"
+)
+
+func TestMain(m *testing.M) {
+	logger.InitNop()
+	code := m.Run()
+	os.Exit(code)
+}
+
+func TestResponseAdaptor(t *testing.T) {
+	yamlSpec := `
+kind: ResponseAdaptor
+name: ra
+header:
+  del: ["X-Del"]
+  add:
+    "[[headerName]]": "mockedHeaderValue"
+body: "copyright [[name]]"
+`
+
+	ra := doTest(t, yamlSpec, nil)
+
+	yamlSpec = `
+kind: ResponseAdaptor
+name: ra
+header:
+  del: ["X-Del"]
+  add:
+    "[[headerName]]": "mockedHeaderValue"
+body: "copyright megaease"
+`
+	doTest(t, yamlSpec, ra)
+
+	yamlSpec = `
+kind: ResponseAdaptor
+name: ra
+header:
+  del: ["X-Del"]
+  add:
+    "[[headerName]]": "mockedHeaderValue"
+`
+	doTest(t, yamlSpec, nil)
+}
+
+func doTest(t *testing.T, yamlSpec string, prev *ResponseAdaptor) *ResponseAdaptor {
+	rawSpec := make(map[string]interface{})
+	yamltool.Unmarshal([]byte(yamlSpec), &rawSpec)
+
+	spec, e := httppipeline.NewFilterSpec(rawSpec, nil)
+	if e != nil {
+		t.Errorf("unexpected error: %v", e)
+	}
+
+	ra := &ResponseAdaptor{}
+	if prev == nil {
+		ra.Init(spec)
+	} else {
+		ra.Inherit(spec, prev)
+	}
+
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedTemplate = func() texttemplate.TemplateEngine {
+		tt, _ := texttemplate.NewDefault([]string{"name", "headerName"})
+		tt.SetDict("name", "megaease")
+		tt.SetDict("headerName", "mockedHeader")
+		return tt
+	}
+	resp := httptest.NewRecorder()
+	resp.Header().Add("X-Del", "deleted")
+
+	ctx.MockedResponse.MockedSetBody = func(body io.Reader) {
+		data, _ := io.ReadAll(body)
+		resp.Write(data)
+	}
+	ctx.MockedResponse.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(resp.Header())
+	}
+	ctx.MockedCallNextHandler = func(lastResult string) string {
+		return ""
+	}
+
+	ra.Handle(ctx)
+
+	if v := resp.Header().Get("mockedHeader"); v != "mockedHeaderValue" {
+		t.Error("unexpected header name or value: ", v)
+	}
+
+	if v := resp.Header().Get("X-Del"); v != "" {
+		t.Error("header 'X-Del' should not exist")
+	}
+
+	if v := resp.Body.String(); ra.spec.Body != "" && v != "copyright megaease" {
+		t.Error("unexpected body:", v)
+	}
+
+	ra.Status()
+	ra.Description()
+	return ra
+}
diff --git a/pkg/filter/timelimiter/timelimiter_test.go b/pkg/filter/timelimiter/timelimiter_test.go
new file mode 100644
index 00000000..91cd4200
--- /dev/null
+++ b/pkg/filter/timelimiter/timelimiter_test.go
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package timelimiter
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/logger"
+	"github.com/megaease/easegress/pkg/object/httppipeline"
+	"github.com/megaease/easegress/pkg/util/yamltool"
+)
+
+func TestMain(m *testing.M) {
+	logger.InitNop()
+	code := m.Run()
+	os.Exit(code)
+}
+
+func TestTimeLimiter(t *testing.T) {
+	const yamlSpec = `
+kind: TimeLimiter
+name: timelimiter
+defaultTimeoutDuration: 456ms
+urls:
+  - timeoutDuration: 10ms
+    methods: [GET]
+    url:
+      exact: /timelimit
+  - methods: [POST]
+    url:
+      prefix: /customer
+`
+	rawSpec := make(map[string]interface{})
+	yamltool.Unmarshal([]byte(yamlSpec), &rawSpec)
+
+	spec, e := httppipeline.NewFilterSpec(rawSpec, nil)
+	if e != nil {
+		t.Errorf("unexpected error: %v", e)
+	}
+
+	tl := &TimeLimiter{}
+	tl.Init(spec)
+
+	if tl.spec.defaultTimeout != 456*time.Millisecond {
+		t.Error("default timeout duration is not the value in spec")
+	}
+
+	if len(tl.spec.URLs) != 2 {
+		t.Error("the length of 'urls' should be 2")
+	} else if tl.spec.URLs[0].timeout != 10*time.Millisecond {
+		t.Error("timeout duration is not the value in spec")
+	}
+
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedMethod = func() string {
+		return http.MethodGet
+	}
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/timelimit"
+	}
+	ctx.MockedResponse.MockedStd = func() http.ResponseWriter {
+		return &httptest.ResponseRecorder{}
+	}
+	ctx.MockedCallNextHandler = func(lastResult string) string {
+		return ""
+	}
+
+	result := tl.Handle(ctx)
+	if result == resultTimeout {
+		t.Error("timeout should not happen")
+	}
+
+	ctx.MockedCallNextHandler = func(lastResult string) string {
+		time.Sleep(20 * time.Millisecond)
+		return ""
+	}
+
+	result = tl.Handle(ctx)
+	if result != resultTimeout {
+		t.Error("expected timeout didn't happen")
+	}
+
+	ctx.MockedRequest.MockedPath = func() string {
+		return "/notimelimit"
+	}
+
+	result = tl.Handle(ctx)
+	if result == resultTimeout {
+		t.Error("request path doesn't match, timeout should not happen")
+	}
+
+	if tl.Status() != nil {
+		t.Error("behavior changed, please update this case")
+	}
+	tl.Description()
+
+	newTl := &TimeLimiter{}
+	spec, _ = httppipeline.NewFilterSpec(rawSpec, nil)
+	newTl.Inherit(spec, tl)
+	tl.Close()
+	result = newTl.Handle(ctx)
+	if result == resultTimeout {
+		t.Error("request path doesn't match, timeout should not happen")
+	}
+}
diff --git a/pkg/filter/validator/validator_test.go b/pkg/filter/validator/validator_test.go
new file mode 100644
index 00000000..054abb98
--- /dev/null
+++ b/pkg/filter/validator/validator_test.go
@@ -0,0 +1,291 @@
+/*
+ * Copyright (c) 2017, MegaEase
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package validator
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/megaease/easegress/pkg/context/contexttest"
+	"github.com/megaease/easegress/pkg/logger"
+	"github.com/megaease/easegress/pkg/object/httppipeline"
+	"github.com/megaease/easegress/pkg/util/httpheader"
+	"github.com/megaease/easegress/pkg/util/yamltool"
+)
+
+func TestMain(m *testing.M) {
+	logger.InitNop()
+	code := m.Run()
+	os.Exit(code)
+}
+
+func createValidator(yamlSpec string, prev *Validator) *Validator {
+	rawSpec := make(map[string]interface{})
+	yamltool.Unmarshal([]byte(yamlSpec), &rawSpec)
+	spec, _ := httppipeline.NewFilterSpec(rawSpec, nil)
+	v := &Validator{}
+	if prev == nil {
+		v.Init(spec)
+	} else {
+		v.Inherit(spec, prev)
+	}
+	return v
+}
+
+func TestHeaders(t *testing.T) {
+	const yamlSpec = `
+kind: Validator
+name: validator
+headers:
+  Is-Valid:
+    values: ["abc", "goodplan"]
+    regexp: "^ok-.+$"
+`
+
+	v := createValidator(yamlSpec, nil)
+
+	header := http.Header{}
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+
+	result := v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("request has no header 'Is-Valid', should be invalid")
+	}
+
+	header.Add("Is-Valid", "Invalid")
+	result = v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("request has header 'Is-Valid', but value is incorrect, should be invalid")
+	}
+
+	header.Set("Is-Valid", "goodplan")
+	result = v.Handle(ctx)
+	if result == resultInvalid {
+		t.Errorf("request has header 'Is-Valid' and value is correct, should be valid")
+	}
+
+	header.Set("Is-Valid", "ok-1")
+	result = v.Handle(ctx)
+	if result == resultInvalid {
+		t.Errorf("request has header 'Is-Valid' and matches the regular expression, should be valid")
+	}
+}
+
+func TestJWT(t *testing.T) {
+	const yamlSpec = `
+kind: Validator
+name: validator
+jwt:
+  cookieName: auth
+  algorithm: HS256
+  secret: 313233343536
+`
+	v := createValidator(yamlSpec, nil)
+
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedCookie = func(name string) (*http.Cookie, error) {
+		return nil, fmt.Errorf("not exist")
+	}
+	header := http.Header{}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+
+	token := "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.3Ywq9NlR3cBST4nfcdbR-fcZ8374RHzU50X6flKvG-tnWFMalMaHRm3cMpXs1NrZ"
+	header.Set("Authorization", "Bearer "+token)
+	result := v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("the jwt token in header should be invalid")
+	}
+
+	token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU"
+	header.Set("Authorization", "Bearer "+token)
+	result = v.Handle(ctx)
+	if result == resultInvalid {
+		t.Errorf("the jwt token in header should be valid")
+	}
+
+	header.Set("Authorization", "not Bearer "+token)
+	result = v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("the jwt token in header should be invalid")
+	}
+
+	header.Set("Authorization", "Bearer "+token+"abc")
+	result = v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("the jwt token in header should be invalid")
+	}
+
+	header.Del("Authorization")
+	ctx.MockedRequest.MockedCookie = func(name string) (*http.Cookie, error) {
+		return &http.Cookie{Value: token}, nil
+	}
+	result = v.Handle(ctx)
+	if result == resultInvalid {
+		t.Errorf("the jwt token in cookie should be valid")
+	}
+
+	v = createValidator(yamlSpec, v)
+	result = v.Handle(ctx)
+	if result == resultInvalid {
+		t.Errorf("the jwt token in cookie should be valid")
+	}
+
+	if v.Status() != nil {
+		t.Error("behavior changed, please update this case")
+	}
+	v.Description()
+}
+
+func TestOAuth2JWT(t *testing.T) {
+	const yamlSpec = `
+kind: Validator
+name: validator
+oauth2:
+  jwt:
+    algorithm: HS256
+    secret: 313233343536
+`
+	v := createValidator(yamlSpec, nil)
+
+	ctx := &contexttest.MockedHTTPContext{}
+
+	header := http.Header{}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+
+	token := "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.3Ywq9NlR3cBST4nfcdbR-fcZ8374RHzU50X6flKvG-tnWFMalMaHRm3cMpXs1NrZ"
+	header.Set("Authorization", "Bearer "+token)
+	result := v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("OAuth/2 Authorization should fail")
+	}
+
+	token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6Im1lZ2FlYXNlIn0.HRcRwN6zLJnubaUnZhZ5jC-j-rRiT-5mY8emJW6h6so"
+	header.Set("Authorization", "Bearer "+token)
+	result = v.Handle(ctx)
+	if result == resultInvalid {
+		t.Errorf("OAuth/2 Authorization should succeed")
+	}
+
+	header.Set("Authorization", "not Bearer "+token)
+	result = v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("OAuth/2 Authorization should fail")
+	}
+
+	header.Set("Authorization", "Bearer "+token+"abc")
+	result = v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("OAuth/2 Authorization should fail")
+	}
+}
+
+func TestOAuth2TokenIntrospect(t *testing.T) {
+	yamlSpec := `
+kind: Validator
+name: validator
+oauth2:
+  tokenIntrospect:
+    endPoint: http://oauth2.megaease.com/
+    insecureTls: true
+    clientId: megaease
+    clientSecret: secret
+`
+	v := createValidator(yamlSpec, nil)
+	ctx := &contexttest.MockedHTTPContext{}
+
+	header := http.Header{}
+	ctx.MockedRequest.MockedHeader = func() *httpheader.HTTPHeader {
+		return httpheader.New(header)
+	}
+
+	token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6Im1lZ2FlYXNlIn0.HRcRwN6zLJnubaUnZhZ5jC-j-rRiT-5mY8emJW6h6so"
+	header.Set("Authorization", "Bearer "+token)
+
+	body := `{
+			"subject":"megaease.com",
+			"scope":"read,write",
+			"active": false
+		}`
+	fnSendRequest = func(client *http.Client, r *http.Request) (*http.Response, error) {
+		reader := strings.NewReader(body)
+		return &http.Response{
+			Body: io.NopCloser(reader),
+		}, nil
+	}
+	result := v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("OAuth/2 Authorization should fail")
+	}
+
+	yamlSpec = `
+kind: Validator
+name: validator
+oauth2:
+  tokenIntrospect:
+    endPoint: http://oauth2.megaease.com/
+    clientId: megaease
+    clientSecret: secret
+    basicAuth: megaease@megaease
+`
+	v = createValidator(yamlSpec, nil)
+
+	body = `{
+			"subject":"megaease.com",
+			"scope":"read,write",
+			"active": true
+		}`
+	result = v.Handle(ctx)
+	if result == resultInvalid {
+		t.Errorf("OAuth/2 Authorization should succeed")
+	}
+}
+
+func TestSignature(t *testing.T) {
+	// This test is almost covered by signer
+
+	const yamlSpec = `
+kind: Validator
+name: validator
+signature:
+  accessKeys:
+    AKID: SECRET
+`
+	v := createValidator(yamlSpec, nil)
+
+	ctx := &contexttest.MockedHTTPContext{}
+	ctx.MockedRequest.MockedStd = func() *http.Request {
+		r, _ := http.NewRequest(http.MethodGet, "http://megaease.com", nil)
+		return r
+	}
+
+	result := v.Handle(ctx)
+	if result != resultInvalid {
+		t.Errorf("OAuth/2 Authorization should fail")
+	}
+}
diff --git a/pkg/object/function/spec/function_test.go b/pkg/object/function/spec/function_test.go
index 0934ba72..979ad276 100644
--- a/pkg/object/function/spec/function_test.go
+++ b/pkg/object/function/spec/function_test.go
@@ -45,6 +45,24 @@ func TestInValidSpec(t *testing.T) {
 }
 
 func TestInvalidKnative(t *testing.T) {
+	spec := Spec{
+		Name:           "demo",
+		Image:          "helloworld:1.0",
+		Port:           10000,
+		AutoScaleType:  AutoScaleMetricCPU,
+		AutoScaleValue: "0.1",
+		MinReplica:     1,
+		MaxReplica:     2,
+		LimitCPU:       "aaa",
+		LimitMemory:    "80Mi",
+		RequestCPU:     "100m",
+		RequestMemory:  "60Mi",
+	}
+
+	var err error
+	if err = spec.Validate(); err == nil {
+		t.Errorf("test failed spec should not be valid, err: %v", err)
+	}
 }
 
 func TestValidSpec(t *testing.T) {
@@ -67,3 +85,102 @@ func TestValidSpec(t *testing.T) {
 		t.Errorf("test failed spec should not be valid, err: %v", err)
 	}
 }
+
+func TestInValidSpecReplica(t *testing.T) {
+	spec := Spec{
+		Name:           "demo",
+		Image:          "helloworld:1.0",
+		Port:           10000,
+		AutoScaleType:  AutoScaleMetricCPU,
+		AutoScaleValue: "0.1",
+		MinReplica:     2,
+		MaxReplica:     1,
+		LimitCPU:       "180m",
+		LimitMemory:    "80Mi",
+		RequestCPU:     "100m",
+		RequestMemory:  "60Mi",
+	}
+
+	var err error
+	if err = spec.Validate(); err == nil {
+		t.Errorf("test failed spec should not be valid, err: %v", err)
+	}
+}
+
+func TestInValidSpecAutoScaleType(t *testing.T) {
+	spec := Spec{
+		Name:           "demo",
+		Image:          "helloworld:1.0",
+		Port:           10000,
+		AutoScaleType:  "nothing",
+		AutoScaleValue: "0.1",
+		MinReplica:     2,
+		MaxReplica:     1,
+		LimitCPU:       "180m",
+		LimitMemory:    "80Mi",
+		RequestCPU:     "100m",
+		RequestMemory:  "60Mi",
+	}
+
+	var err error
+	if err = spec.Validate(); err == nil {
+		t.Errorf("test failed spec should not be invalid, err: %v", err)
+	}
+	fmt.Println("err is ", err)
+}
+
+func TestNext(t *testing.T) {
+	fsm, _ := InitFSM(InitState())
+
+	f := &Function{
+		Spec: &Spec{
+			Name:           "demo",
+			Image:          "helloworld:1.0",
+			Port:           10000,
+			AutoScaleValue: "0.1",
+			MinReplica:     1,
+			MaxReplica:     2,
+			LimitCPU:       "180m",
+			LimitMemory:    "80Mi",
+			RequestCPU:     "100m",
+			RequestMemory:  "60Mi",
+		},
+		Fsm:    fsm,
+		Status: &Status{},
+	}
+
+	updated, err := f.Next(ReadyEvent)
+
+	if err != nil {
+		t.Errorf("test failed Next should succ, err: %v", err)
+	}
+
+	t.Logf("updated: %v, new status: %s\n", updated, f.Fsm.Current())
+}
+
+func TestInvalidNext(t *testing.T) {
+	fsm, _ := InitFSM(InitState())
+
+	f := &Function{
+		Spec: &Spec{
+			Name:           "demo",
+			Image:          "helloworld:1.0",
+			Port:           10000,
+			AutoScaleValue: "0.1",
+			MinReplica:     1,
+			MaxReplica:     2,
+			LimitCPU:       "180m",
+			LimitMemory:    "80Mi",
+			RequestCPU:     "100m",
+			RequestMemory:  "60Mi",
+		},
+		Fsm:    fsm,
+		Status: &Status{},
+	}
+
+	_, err := f.Next(StartEvent)
+
+	if err == nil {
+		t.Errorf("test failed Next should failed, start event will be rejected in initial state")
+	}
+}
diff --git a/pkg/object/meshcontroller/spec/spec_test.go b/pkg/object/meshcontroller/spec/spec_test.go
index fbfd8086..028577db 100644
--- a/pkg/object/meshcontroller/spec/spec_test.go
+++ b/pkg/object/meshcontroller/spec/spec_test.go
@@ -20,7 +20,6 @@ package spec
 import (
 	"fmt"
 	"os"
-	"path/filepath"
 	"testing"
 
 	"github.com/megaease/easegress/pkg/filter/circuitbreaker"
@@ -29,25 +28,13 @@ import (
 	"github.com/megaease/easegress/pkg/filter/retryer"
 	"github.com/megaease/easegress/pkg/filter/timelimiter"
 	"github.com/megaease/easegress/pkg/logger"
-	"github.com/megaease/easegress/pkg/option"
+	_ "github.com/megaease/easegress/pkg/object/httpserver"
 	"github.com/megaease/easegress/pkg/util/urlrule"
 )
 
-const tempDir = "/tmp/eg-test"
-
 func TestMain(m *testing.M) {
-	absLogDir := filepath.Join(tempDir, "global-log")
-	os.MkdirAll(absLogDir, 0o755)
-	logger.Init(&option.Options{
-		Name:      "meshspec-for-log",
-		AbsLogDir: absLogDir,
-	})
-
+	logger.InitNop()
 	code := m.Run()
-
-	logger.Sync()
-	os.RemoveAll(tempDir)
-
 	os.Exit(code)
 }
 
@@ -73,6 +60,32 @@ func TestSideCarIngressPipelineSpec(t *testing.T) {
 	fmt.Println(superSpec.YAMLConfig())
 }
 
+func TestAdminInValidat(t *testing.T) {
+	a := Admin{
+		RegistryType:      "unknow",
+		HeartbeatInterval: "10s",
+	}
+
+	err := a.Validate()
+
+	if err == nil {
+		t.Errorf("registry type is invalid, should failed")
+	}
+}
+
+func TestAdminValidat(t *testing.T) {
+	a := Admin{
+		RegistryType:      "eureka",
+		HeartbeatInterval: "10s",
+	}
+
+	err := a.Validate()
+
+	if err != nil {
+		t.Errorf("registry type is valid, err: %v", err)
+	}
+}
+
 func TestSideCarEgressPipelineSpec(t *testing.T) {
 	s := &Service{
 		Name: "order-001",
@@ -184,6 +197,99 @@ func TestSideCarEgressPipelineWithCanarySpec(t *testing.T) {
 	fmt.Println(superSpec.YAMLConfig())
 }
 
+func TestSideCarEgressPipelneNotLoadBalancer(t *testing.T) {
+	s := &Service{
+		Name: "order-003-canary-array",
+		Sidecar: &Sidecar{
+			Address:         "127.0.0.1",
+			IngressPort:     8080,
+			IngressProtocol: "http",
+			EgressPort:      9090,
+			EgressProtocol:  "http",
+		},
+
+		Canary: &Canary{
+			CanaryRules: []*CanaryRule{
+				{
+					Headers: map[string]*urlrule.StringMatch{
+						"X-canary": {
+							Exact: "lv1",
+						},
+					},
+					URLs: []*urlrule.URLRule{
+						{
+							Methods: []string{
+								"GET",
+								"POST",
+							},
+							URL: urlrule.StringMatch{
+								Prefix: "/",
+							},
+						},
+					},
+					ServiceInstanceLabels: map[string]string{
+						"version": "v1",
+					},
+				},
+				{
+					Headers: map[string]*urlrule.StringMatch{
+						"X-canary": {
+							Exact: "ams",
+						},
+					},
+					URLs: []*urlrule.URLRule{
+						{
+							Methods: []string{
+								"GET",
+								"POST",
+							},
+							URL: urlrule.StringMatch{
+								Prefix: "/",
+							},
+						},
+					},
+					ServiceInstanceLabels: map[string]string{
+						"version": "v2",
+					},
+				},
+			},
+		},
+	}
+
+	instanceSpecs := []*ServiceInstanceSpec{
+		{
+			ServiceName: "fake-001",
+			InstanceID:  "xxx-89757",
+			IP:          "192.168.0.110",
+			Port:        80,
+			Status:      "UP",
+		},
+		{
+			ServiceName: "fake-002-canary",
+			InstanceID:  "zzz-73597",
+			IP:          "192.168.0.120",
+			Port:        80,
+			Status:      "UP",
+			Labels: map[string]string{
+				"version": "v1",
+			},
+		},
+		{
+			ServiceName: "fake-003-canary-no-match",
+			InstanceID:  "yyy-73587",
+			IP:          "192.168.0.121",
+			Port:        80,
+			Status:      "UP",
+			Labels: map[string]string{
+				"version": "v2",
+			},
+		},
+	}
+
+	superSpec, _ := s.SideCarEgressPipelineSpec(instanceSpecs)
+	fmt.Println(superSpec.YAMLConfig())
+}
+
 func TestSideCarEgressPipelineWithMultipleCanarySpec(t *testing.T) {
 	s := &Service{
 		Name: "order-003-canary-array",
@@ -429,6 +535,26 @@ func TestSideCarEgressPipelineWithCanaryInstanceMultipleLabelSpec(t *testing.T)
 	fmt.Println(superSpec.YAMLConfig())
 }
 
+func TestIngressHTTPServerSpec(t *testing.T) {
+	rule := []*IngressRule{
+		{
+			Host: "megaease.com",
+			Paths: []*IngressPath{
+				{
+					Path:    "/",
+					Backend: "portal",
+				},
+			},
+		},
+	}
+
+	_, err := IngressHTTPServerSpec(1233, rule)
+
+	if err != nil {
+		t.Errorf("ingress http server spec failed: %v", err)
+	}
+
+}
 func TestSideCarIngressWithResiliencePipelineSpec(t *testing.T) {
 	s := &Service{
 		Name: "order-001",
@@ -575,3 +701,234 @@ func TestSideCarEgressResiliencePipelineSpec(t *testing.T) {
 	superSpec, _ := s.SideCarEgressPipelineSpec(instanceSpecs)
 	fmt.Println(superSpec.YAMLConfig())
 }
+
+func TestPipelineBuilderFailed(t *testing.T) {
+	builder := newPipelineSpecBuilder("abc")
+
+	builder.appendRateLimiter(nil)
+
+	builder.appendCircuitBreaker(nil)
+
+	builder.appendRetryer(nil)
+
+	builder.appendTimeLimiter(nil)
+
+	yamlStr := builder.yamlConfig()
+	if len(yamlStr) == 0 {
+		t.Errorf("builder append nil resilience filter failed")
+	}
+}
+
+func TestPipelineBuilder(t *testing.T) {
+	builder := newPipelineSpecBuilder("abc")
+
+	rateLimiter := &ratelimiter.Spec{
+		Policies: []*ratelimiter.Policy{{
+			Name:               "default",
+			TimeoutDuration:    "100ms",
+			LimitForPeriod:     50,
+			LimitRefreshPeriod: "10ms",
+		}},
+		DefaultPolicyRef: "default",
+		URLs: []*ratelimiter.URLRule{{
+			URLRule: urlrule.URLRule{
+				Methods: []string{"GET"},
+				URL: urlrule.StringMatch{
+					Exact:  "/path1",
+					Prefix: "/path2/",
+					RegEx:  "^/path3/[0-9]+$",
+				},
+				PolicyRef: "default",
+			},
+		}},
+	}
+
+	builder.appendRateLimiter(rateLimiter)
+	yaml := builder.yamlConfig()
+
+	if len(yaml) == 0 {
+		t.Errorf("pipeline builder yamlconfig failed")
+	}
+
+}
+
+func TestIngressPipelineSpec(t *testing.T) {
+	s := &Service{
+		Name: "order-001",
+		LoadBalance: &LoadBalance{
+			Policy: proxy.PolicyRandom,
+		},
+		Sidecar: &Sidecar{
+			Address:         "127.0.0.1",
+			IngressPort:     8080,
+			IngressProtocol: "http",
+			EgressPort:      9090,
+			EgressProtocol:  "http",
+		},
+	}
+	instanceSpecs := []*ServiceInstanceSpec{
+		{
+			ServiceName: "fake-001",
+			InstanceID:  "xxx-89757",
+			IP:          "192.168.0.110",
+			Port:        80,
+			Status:      "UP",
+		},
+		{
+			ServiceName: "fake-002",
+			InstanceID:  "zzz-73597",
+			IP:          "192.168.0.120",
+			Port:        80,
+			Status:      "UP",
+		},
+	}
+	superSpec, err := s.IngressPipelineSpec(instanceSpecs)
+
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	fmt.Println(superSpec.YAMLConfig())
+}
+
+func TestSidecarIngressPipelineSpec(t *testing.T) {
+	s := &Service{
+		Name: "order-001",
+		LoadBalance: &LoadBalance{
+			Policy: proxy.PolicyRandom,
+		},
+		Sidecar: &Sidecar{
+			Address:         "127.0.0.1",
+			IngressPort:     8080,
+			IngressProtocol: "http",
+			EgressPort:      9090,
+			EgressProtocol:  "http",
+		},
+	}
+
+	superSpec, err := s.SideCarIngressHTTPServerSpec()
+
+	if err != nil {
+		t.Fatalf("ingress http server spec failed: %v", err)
+	}
+	fmt.Println(superSpec.YAMLConfig())
+
+	superSpec, err = s.SideCarEgressHTTPServerSpec()
+
+	if err != nil {
+		t.Fatalf("egress http server spec failed: %v", err)
+	}
+
+	fmt.Println(superSpec.YAMLConfig())
+}
+
+func TestUniqueCanaryHeadersEmpty(t *testing.T) {
+	s := &Service{
+		Name: "order-001",
+		LoadBalance: &LoadBalance{
+			Policy: proxy.PolicyRandom,
+		},
+		Sidecar: &Sidecar{
+			Address:         "127.0.0.1",
+			IngressPort:     8080,
+			IngressProtocol: "http",
+			EgressPort:      9090,
+			EgressProtocol:  "http",
+		},
+	}
+
+	val := s.UniqueCanaryHeaders()
+	if len(val) > 0 {
+		t.Errorf("canary header should be none")
+	}
+
+}
+
+func TestUniqueCanaryHeaders(t *testing.T) {
+	s := &Service{
+		Name: "order-002-canary",
+		LoadBalance: &LoadBalance{
+			Policy: proxy.PolicyIPHash,
+		},
+		Sidecar: &Sidecar{
+			Address:         "127.0.0.1",
+			IngressPort:     8080,
+			IngressProtocol: "http",
+			EgressPort:      9090,
+			EgressProtocol:  "http",
+		},
+
+		Canary: &Canary{
+			CanaryRules: []*CanaryRule{
+				{
+					Headers: map[string]*urlrule.StringMatch{
+						"X-canary": {
+							Exact: "lv1",
+						},
+					},
+					URLs: []*urlrule.URLRule{
+						{
+							Methods: []string{
+								"GET",
+								"POST",
+							},
+							URL: urlrule.StringMatch{
+								Prefix: "/",
+							},
+						},
+					},
+					ServiceInstanceLabels: map[string]string{
+						"version": "v1",
+					},
+				},
+			},
+		},
+	}
+
+	val := s.UniqueCanaryHeaders()
+	if len(val) == 0 {
+		t.Errorf("canary header should not be none")
+	}
+}
+
+func TestEgressName(t *testing.T) {
+	s := &Service{
+		Name: "order-001",
+		LoadBalance: &LoadBalance{
+			Policy: proxy.PolicyRandom,
+		},
+		Sidecar: &Sidecar{
+			Address:         "127.0.0.1",
+			IngressPort:     8080,
+			IngressProtocol: "http",
+			EgressPort:      9090,
+			EgressProtocol:  "http",
+		},
+	}
+	if len(s.EgressHTTPServerName()) == 0 {
+		t.Error("egress httpserver name should not be none")
+	}
+
+	if len(s.EgressHandlerName()) == 0 {
+		t.Error("egress httpserver handler should not be none")
+	}
+
+	if len(s.IngressHTTPServerName()) == 0 {
+		t.Error("ingress httpserver handler should not be none")
+	}
+
+	if len(s.IngressHandlerName()) == 0 {
+		t.Error("ingress httpserver handler should not be none")
+	}
+
+	if len(s.BackendName()) == 0 {
+		t.Error("backend name should not be none")
+	}
+
+	if len(s.IngressEndpoint()) == 0 {
+		t.Error("ingress endpoint name should not be none")
+	}
+
+	if len(s.EgressEndpoint()) == 0 {
+		t.Error("egress endpoint name should not be none")
+	}
+}
diff --git a/pkg/object/statussynccontroller/timer_test.go b/pkg/object/statussynccontroller/timer_test.go
deleted file mode 100644
index 45e4e850..00000000
--- a/pkg/object/statussynccontroller/timer_test.go
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package statussynccontroller
-
-import (
-	"reflect"
-	"testing"
-	"time"
-)
-
-func TestNextDuration(t *testing.T) {
-	type parameters struct {
-		t             time.Time
-		paceInSeconds int
-	}
-
-	tests := []struct {
-		name  string
-		want  time.Duration
-		input parameters
-	}{
-		{
-			name: "test-1",
-			want: 5*time.Second - 555*time.Nanosecond,
-			input: parameters{
-				t:             time.Unix(0, 555),
-				paceInSeconds: 5,
-			},
-		},
-		{
-			name: "test-2",
-			want: 778 * time.Millisecond,
-			input: parameters{
-				t:             time.Unix(199994, 222000000),
-				paceInSeconds: 5,
-			},
-		},
-		{
-			name: "test-3",
-			want: 5*time.Second - 333*time.Nanosecond,
-			input: parameters{
-				t:             time.Unix(38383835, 333),
-				paceInSeconds: 5,
-			},
-		},
-		{
-			name: "test-4",
-			want: 9*time.Second - 888*time.Nanosecond,
-			input: parameters{
-				t:             time.Unix(99999991, 888),
-				paceInSeconds: 10,
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := nextDuration(tt.input.t, tt.input.paceInSeconds)
-			if !reflect.DeepEqual(tt.want, got) {
-				t.Fatalf("want %v, got %v", tt.want, got)
-			}
-		})
-	}
-}
diff --git a/pkg/util/jmxtool/agent_controller_test.go b/pkg/util/jmxtool/agent_controller_test.go
index 9dee30d2..1e6f8615 100644
--- a/pkg/util/jmxtool/agent_controller_test.go
+++ b/pkg/util/jmxtool/agent_controller_test.go
@@ -26,19 +26,25 @@ import (
 	"time"
 
 	"github.com/megaease/easegress/pkg/filter/proxy"
+	"github.com/megaease/easegress/pkg/logger"
 	"github.com/megaease/easegress/pkg/object/meshcontroller/spec"
 )
 
-func httpServer(finished chan bool) {
+func httpServer(finished chan bool, notFoundFlag bool) {
 	m := http.NewServeMux()
 	s := http.Server{Addr: ":8181", Handler: m}
 	m.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, "Goobey, %q", html.EscapeString(r.URL.Path))
 		s.Shutdown(context.Background())
 	})
-	m.HandleFunc(serviceConfigURL, func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
-	})
+	if !notFoundFlag {
+		m.HandleFunc(serviceConfigURL, func(w http.ResponseWriter, r *http.Request) {
+			fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
+		})
+		m.HandleFunc(canaryConfigURL, func(w http.ResponseWriter, r *http.Request) {
+			fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
+		})
+	}
 	if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
 		fmt.Println(err)
 	}
@@ -46,14 +52,8 @@ func httpServer(finished chan bool) {
 	finished <- true
 }
 
-func TestAgentClient(t *testing.T) {
-	finished := make(chan bool)
-	go httpServer(finished)
-
-	agent := NewAgentClient("127.0.0.1", "8181")
-	fmt.Printf("%v\n", agent)
-
-	service := &spec.Service{
+func getTestService() spec.Service {
+	service := spec.Service{
 		Name: "agent",
 		LoadBalance: &spec.LoadBalance{
 			Policy: proxy.PolicyRandom,
@@ -66,17 +66,75 @@ func TestAgentClient(t *testing.T) {
 			EgressProtocol:  "http",
 		},
 	}
+	return service
+}
+
+func TestAgentClientSuccess(t *testing.T) {
+	logger.InitNop()
+	finished := make(chan bool)
+	go httpServer(finished, false)
+
+	agent := NewAgentClient("127.0.0.1", "8181")
+	fmt.Printf("%+v\n", agent)
+
+	service := getTestService()
+	// UpdateService check
+	err := agent.UpdateService(&service, 1)
+	if err != nil {
+		t.Errorf("agent update service failed\n")
+	}
 
-	agent.UpdateService(service, 1)
+	// UpdateCanary
 	header := &spec.GlobalCanaryHeaders{
 		ServiceHeaders: map[string][]string{},
 	}
-	agent.UpdateCanary(header, 1)
+	err = agent.UpdateCanary(header, 1)
+	if err != nil {
+		t.Errorf("agent update canary failed\n")
+	}
 
+	// shutdown
 	var client = &http.Client{
 		Timeout: time.Second,
 	}
 	client.Get("http://127.0.0.1:8181/shutdown")
+	<-finished
+}
 
+func TestAgentClientFail(t *testing.T) {
+	logger.InitNop()
+	agent := NewAgentClient("127.0.0.1", "8181")
+	service := getTestService()
+
+	// test without available service
+	err := agent.UpdateService(&service, 1)
+	if err == nil {
+		t.Errorf("agent should fail\n")
+	}
+	header := &spec.GlobalCanaryHeaders{
+		ServiceHeaders: map[string][]string{},
+	}
+	err = agent.UpdateCanary(header, 1)
+	if err == nil {
+		t.Errorf("agent should fail\n")
+	}
+
+	// test with 404
+	finished := make(chan bool)
+	go httpServer(finished, true)
+
+	err = agent.UpdateService(&service, 1)
+	if err == nil {
+		t.Errorf("agent shoudl fail\n")
+	}
+	err = agent.UpdateCanary(header, 1)
+	if err == nil {
+		t.Errorf("agent shoudl fail\n")
+	}
+	// shutdown
+	var client = &http.Client{
+		Timeout: time.Second,
+	}
+	client.Get("http://127.0.0.1:8181/shutdown")
 	<-finished
 }
diff --git a/pkg/common/strings_test.go b/pkg/util/jmxtool/common_test.go
similarity index 55%
rename from pkg/common/strings_test.go
rename to pkg/util/jmxtool/common_test.go
index 920ca03d..d693e4ef 100644
--- a/pkg/common/strings_test.go
+++ b/pkg/util/jmxtool/common_test.go
@@ -14,24 +14,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package common
+
+package jmxtool
 
 import (
+	"reflect"
 	"testing"
 )
 
-func TestStrings(t *testing.T) {
-	str1 := "Hello, World"
-	b := S2b(str1)
-	str2 := B2s(b)
-	if str1 != str2 {
-		t.Errorf("expected %s, result %s", str1, str2)
+func Test_extractKVs(t *testing.T) {
+	// extractKVs for int
+	prefix := "test"
+	want := []map[string]string{{prefix: "1"}}
+	got := extractKVs(prefix, 1)
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("error for extract int")
 	}
 
-	b = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100}
-	s := B2s(b)
-	if s != str1 {
-		t.Errorf("expected %s, result %s", str1, s)
+	// extractKVs for interface array
+	want = []map[string]string{
+		{prefix + ".0": "1"},
+		{prefix + ".1": "2"},
+		{prefix + ".2": "3"},
+	}
+	got = extractKVs(prefix, []interface{}{1, 2, 3})
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("error for extract []interface")
 	}
-
 }
diff --git a/pkg/util/jmxtool/jmx_controller_test.go b/pkg/util/jmxtool/jmx_controller_test.go
deleted file mode 100644
index 88c35cee..00000000
--- a/pkg/util/jmxtool/jmx_controller_test.go
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (c) 2017, MegaEase
- * All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package jmxtool
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/fatih/structs"
-	yamljsontool "github.com/ghodss/yaml"
-	"gopkg.in/yaml.v2"
-
-	"github.com/megaease/easegress/pkg/object/meshcontroller/spec"
-)
-
-type heapMemoryUsage struct {
-	committed int64
-	init      int64
-	max       int64
-	used      int64
-}
-
-func TestGetMbeanAttribute(t *testing.T) {
-	client := NewJolokiaClient("127.0.0.1", "8778", "jolokia")
-
-	// Read Value
-	oldThreadCount, _ := client.SetMbeanAttribute("com.easeagent.jmx:type=SystemConfig", "ThreadCount", "", 11)
-	fmt.Println(oldThreadCount)
-
-	// Set value
-	newThreadCount, _ := client.GetMbeanAttribute("com.easeagent.jmx:type=SystemConfig", "ThreadCount", "")
-	fmt.Println(newThreadCount)
-
-	newHeapMemoryUsage := heapMemoryUsage{
-		init:      0,
-		committed: 1234,
-		max:       9999,
-		used:      6666,
-	}
-
-	// Set value
-	oldMemoryUsage, _ := client.SetMbeanAttribute("com.easeagent.jmx:type=SystemConfig", "HeapMemoryUsage", "", newHeapMemoryUsage)
-	fmt.Println(oldMemoryUsage)
-
-	// Read sub field of mbean
-	newCommitted, _ := client.GetMbeanAttribute("com.easeagent.jmx:type=SystemConfig", "HeapMemoryUsage", "committed")
-	fmt.Println(newCommitted)
-
-	// List mbean
-	//mbeanDetail, err := client.ListMbean("com.easeagent.jmx:type=SystemConfig")
-	//if err != nil {
-	//	fmt.Println(err)
-	//}
-	//fmt.Println(mbeanDetail)
-	//
-	//// Search mbeans
-	//mbeans, err := client.SearchMbeans("com.easeagent.jmx:*")
-	//if err != nil {
-	//	fmt.Println(err)
-	//}
-	//fmt.Println(mbeans)
-}
-
-func TestExecuteMbeanOperation(t *testing.T) {
-	// Execute operation
-	client := NewJolokiaClient("127.0.0.1", "8778", "jolokia")
-
-	observability := spec.Observability{}
-	observability.OutputServer = &spec.ObservabilityOutputServer{
-		Enabled:         true,
-		BootstrapServer: "128.0.0.1",
-	}
-
-	observabilityTracingDetail := spec.ObservabilityTracingsDetail{
-		Enabled:       true,
-		ServicePrefix: "agent",
-	}
-
-	observability.Tracings = &spec.ObservabilityTracings{
-		SampleByQPS: 123,
-		Output: spec.ObservabilityTracingsOutputConfig{
-			Topic: "KAFKA",
-		},
-		Request:      observabilityTracingDetail,
-		RemoteInvoke: observabilityTracingDetail,
-		Kafka:        observabilityTracingDetail,
-		Jdbc:         observabilityTracingDetail,
-		Redis:        observabilityTracingDetail,
-		Rabbit:       observabilityTracingDetail,
-	}
-
-	observabilityMetricDetail := spec.ObservabilityMetricsDetail{
-		Enabled:  false,
-		Interval: 1,
-		Topic:    "aaa",
-	}
-	observability.Metrics = &spec.ObservabilityMetrics{
-		Request:        observabilityMetricDetail,
-		JdbcConnection: observabilityMetricDetail,
-		JdbcStatement:  observabilityMetricDetail,
-		Rabbit:         observabilityMetricDetail,
-		Redis:          observabilityMetricDetail,
-		Kafka:          observabilityMetricDetail,
-	}
-
-	m := structs.Map(observability)
-
-	args := []interface{}{m}
-	operation, err := client.ExecuteMbeanOperation("com.easeagent.jmx:type=SystemConfig", "updateConfigs", args)
-	if err != nil {
-		fmt.Println(err)
-	}
-	fmt.Println(operation)
-}
-
-func TestSpecTransform(t *testing.T) {
-	observability := spec.Observability{}
-	observability.OutputServer = &spec.ObservabilityOutputServer{
-		Enabled:         true,
-		BootstrapServer: "128.0.0.1",
-	}
-
-	observabilityTracingDetail := spec.ObservabilityTracingsDetail{
-		Enabled:       true,
-		ServicePrefix: "agent",
-	}
-
-	observability.Tracings = &spec.ObservabilityTracings{
-		SampleByQPS: 123,
-		Output: spec.ObservabilityTracingsOutputConfig{
-			Topic: "KAFKA",
-		},
-		Request:      observabilityTracingDetail,
-		RemoteInvoke: observabilityTracingDetail,
-		Kafka:        observabilityTracingDetail,
-		Jdbc:         observabilityTracingDetail,
-		Redis:        observabilityTracingDetail,
-		Rabbit:       observabilityTracingDetail,
-	}
-
-	observabilityMetricDetail := spec.ObservabilityMetricsDetail{
-		Enabled:  false,
-		Interval: 1,
-		Topic:    "aaa",
-	}
-	observability.Metrics = &spec.ObservabilityMetrics{
-		Request:        observabilityMetricDetail,
-		JdbcConnection: observabilityMetricDetail,
-		JdbcStatement:  observabilityMetricDetail,
-		Rabbit:         observabilityMetricDetail,
-		Redis:          observabilityMetricDetail,
-		Kafka:          observabilityMetricDetail,
-	}
-	service := spec.Service{
-		Observability:  &observability,
-		Name:           "service",
-		RegisterTenant: "order",
-		Resilience:     &spec.Resilience{},
-	}
-
-	buff, err := yaml.Marshal(service)
-	if err != nil {
-		fmt.Println(err)
-	}
-
-	jsonBytes, err := yamljsontool.YAMLToJSON(buff)
-	if err != nil {
-		fmt.Println(err)
-	}
-
-	kvMap, err := JSONToKVMap(string(jsonBytes))
-	for k, v := range kvMap {
-		fmt.Println(k, v)
-	}
-}
diff --git a/pkg/util/jsontool/jsontool_test.go b/pkg/util/jsontool/jsontool_test.go
index bb056f4c..3ec7a33f 100644
--- a/pkg/util/jsontool/jsontool_test.go
+++ b/pkg/util/jsontool/jsontool_test.go
@@ -32,3 +32,11 @@ func TestJSONTrimNull(t *testing.T) {
 		t.Fatalf("want: %s\ngot : %s", want, got)
 	}
 }
+
+func TestTrimNull(t *testing.T) {
+	_, err := TrimNull(nil)
+
+	if err == nil {
+		t.Errorf("trim null should be failed")
+	}
+}
diff --git a/pkg/util/ratelimiter/ratelimiter_test.go b/pkg/util/ratelimiter/ratelimiter_test.go
index 1ebc4703..d604152d 100644
--- a/pkg/util/ratelimiter/ratelimiter_test.go
+++ b/pkg/util/ratelimiter/ratelimiter_test.go
@@ -117,6 +117,24 @@ func TestRateLimiter(t *testing.T) {
 		}
 	}
 
+	limiter.SetState(StateDisabled)
+	if permitted, d := limiter.AcquirePermission(); !permitted {
+		t.Errorf("AcquirePermission should succeeded")
+	} else if d != 0 {
+		t.Errorf("wait duration should not be: %s", d.String())
+	}
+
+	limiter.SetState(StateNormal)
+	for i := 0; i < 30; i++ {
+		permitted, d := limiter.AcquirePermission()
+		if !permitted {
+			t.Errorf("AcquirePermission should succeed: %d", i)
+		}
+		if d != time.Duration(i/policy.LimitForPeriod)*policy.LimitRefreshPeriod {
+			t.Errorf("wait duration of %d should not be: %s", i, d.String())
+		}
+	}
+
 	limiter.SetState(StateLimiting)
 	if permitted, d := limiter.AcquirePermission(); permitted {
 		t.Errorf("AcquirePermission should fail")
@@ -124,6 +142,7 @@ func TestRateLimiter(t *testing.T) {
 		t.Errorf("wait duration should not be: %s", d.String())
 	}
 
+	limiter.SetState(StateLimiting)
 	now = now.Add(time.Millisecond * 5)
 	if permitted, d := limiter.AcquirePermission(); permitted {
 		t.Errorf("AcquirePermission should fail")
diff --git a/pkg/util/texttemplate/texttemplate_test.go b/pkg/util/texttemplate/texttemplate_test.go
index 70c2f432..7d5ad425 100644
--- a/pkg/util/texttemplate/texttemplate_test.go
+++ b/pkg/util/texttemplate/texttemplate_test.go
@@ -21,6 +21,102 @@ import (
 	"testing"
 )
 
+func TestNewFailed(t *testing.T) {
+	_, err := New("", "", "", []string{})
+
+	if err == nil {
+		t.Error("new template should not succ")
+	}
+}
+
+func TestNewFailedAtBuild(t *testing.T) {
+	_, err := New(DefaultBeginToken, DefaultEndToken, DefaultSeparator, []string{
+		"filter.xx.",
+	})
+	if err == nil {
+		t.Error("new template should not succ")
+	}
+}
+
+func TestSetDictFailed(t *testing.T) {
+	tt, err := NewDefault([]string{
+		"filter.{}.req.path",
+		"filter.{}.req.method",
+		"filter.{}.req.body",
+		"filter.{}.req.scheme",
+		"filter.{}.req.proto",
+		"filter.{}.req.host",
+		"filter.{}.req.body.{gjson}",
+		"filter.{}.req.header.{}",
+		"filter.{}.rsp.statuscode",
+		"filter.{}.rsp.body.{gjson}",
+	})
+	if err != nil {
+		t.Errorf("new engine failed err %v", err)
+	}
+
+	if err = tt.SetDict("no.abc.req.body", "kkk"); err == nil {
+		t.Errorf("set dict should failed")
+	}
+}
+
+func TestNewDefaultTextTemplateFailed(t *testing.T) {
+	tt, err := NewDefault([]string{
+		"filter.{}.req.path",
+		"filter.{}.req.method",
+		"filter.{}.req.body",
+		"filter.{}.req.scheme",
+		"filter.{}.req.proto",
+		"filter.{}.req.host",
+		"filter.{}.req.body.{gjson}",
+		"filter.{}.req.header.{}",
+		"filter.{}.rsp.statuscode",
+		"filter.{}.rsp.body.{gjson}",
+	})
+	if err != nil {
+		t.Errorf("new engine failed err %v", err)
+	}
+
+	if err = tt.SetDict("filter.abc.req.body", "kkk"); err != nil {
+		t.Errorf("set failed err =%v", err)
+	}
+
+	if s, _ := tt.Render("xxx-[[filter.abc.rsp.body]]--yyy"); s == "xxx-kkk--yyy" {
+		t.Errorf("rendering should fail , but succ")
+	}
+}
+
+func TestNewDefaultTextTemplateNothing(t *testing.T) {
+	tt, err := NewDefault([]string{
+		"filter.{}.req.path",
+		"filter.{}.req.method",
+		"filter.{}.req.body",
+		"filter.{}.req.scheme",
+		"filter.{}.req.proto",
+		"filter.{}.req.host",
+		"filter.{}.req.body.{gjson}",
+		"filter.{}.req.header.{}",
+		"filter.{}.rsp.statuscode",
+		"filter.{}.rsp.body.{gjson}",
+	})
+	if err != nil {
+		t.Errorf("new engine failed err %v", err)
+	}
+
+	if res := tt.MatchMetaTemplate("filter.abc.req.body"); len(res) == 0 {
+		t.Errorf("input %s match template %s", "filter.abc.req.body", res)
+	}
+
+	if err = tt.SetDict("filter.abc.req.body", "kkk"); err != nil {
+		t.Errorf("set failed err =%v", err)
+	}
+
+	if s, err := tt.Render("xxx---yyy"); s != "xxx---yyy" || err != nil {
+		t.Errorf("rendering fail , result is %s expect xxx---yyy", s)
+	}
+
+}
+
 func TestNewDefaultTextTemplateSucc(t *testing.T) {
 	tt, err := NewDefault([]string{
 		"filter.{}.req.path",
@@ -49,6 +145,33 @@ func TestNewDefaultTextTemplateSucc(t *testing.T) {
 	if s, err := tt.Render("xxx-[[filter.abc.req.body]]--yyy"); s != "xxx-kkk--yyy" || err != nil {
 		t.Errorf("rendering fail , result is %s expect xxx-kkk--yyy", s)
 	}
+
+	dict := tt.GetDict()
+	if len(dict) == 0 {
+		t.Error("get dict failed, should not empty")
+	}
+}
+
+func TestNewDefaultTextTemplateEmpty(t *testing.T) {
+	tt, err := NewDefault([]string{
+		"filter.{}.req.path",
+		"filter.{}.req.method",
+		"filter.{}.req.body",
+		"filter.{}.req.scheme",
+		"filter.{}.req.proto",
+		"filter.{}.req.host",
+		"filter.{}.req.body.{gjson}",
+		"filter.{}.req.header.{}",
+		"filter.{}.rsp.statuscode",
+		"filter.{}.rsp.body.{gjson}",
+	})
+	if err != nil {
+		t.Errorf("new engine failed err %v", err)
+	}
+
+	if s, _ := tt.Render("xxx-[[filter.abc.req.body]]--yyy"); s != "xxx---yyy" {
+		t.Errorf("rendering fail , result is %s expect xxx---yyy", s)
+	}
 }
 
 func TestDummyTemplate_render(t *testing.T) {
diff --git a/pkg/util/urlrule/urlrule_test.go b/pkg/util/urlrule/urlrule_test.go
index f6b9b5c4..59d9cf71 100644
--- a/pkg/util/urlrule/urlrule_test.go
+++ b/pkg/util/urlrule/urlrule_test.go
@@ -236,6 +236,15 @@ func TestURLRULENoMatchURL(t *testing.T) {
 	}
 }
 
+func TestFailStringMatch(t *testing.T) {
+	sm := StringMatch{}
+
+	err := sm.Validate()
+	if err == nil {
+		t.Error("string matcher should be invalid")
+	}
+}
+
 func TestDeepEqual(t *testing.T) {
 	u1 := &URLRule{
 		id: "TestDeepEqual1",
@@ -282,6 +291,42 @@ func TestDeepEqual(t *testing.T) {
 		},
 	}
 
+	u5 := &URLRule{
+		id: "TestDeepEqual1",
+		Methods: []string{
+			"GET",
+			"POST",
+		},
+		URL: StringMatch{
+			Exact: "/app/v3/user",
+		},
+	}
+
+	u6 := &URLRule{
+		id: "TestDeepEqual2",
+		Methods: []string{
+			"GET",
+			"POST",
+		},
+		URL: StringMatch{
+			Exact:  "/app/v2/user",
+			Prefix: "/app",
+		},
+	}
+
+	u7 := &URLRule{
+		id: "TestDeepEqual2",
+		Methods: []string{
+			"GET",
+			"POST",
+		},
+		URL: StringMatch{
+			Exact:  "/app/v2/user",
+			Prefix: "/app",
+			RegEx:  "^.*$",
+		},
+	}
+
 	if !u1.DeepEqual(u2) {
 		t.Errorf("%v and %v should be equal\n", u1, u2)
 	}
@@ -291,4 +336,16 @@ func TestDeepEqual(t *testing.T) {
 	if u3.DeepEqual(u4) {
 		t.Errorf("%v and %v should not be equal\n", u3, u4)
 	}
+
+	if u1.DeepEqual(u5) {
+		t.Errorf("%v and %v should not be equal\n", u1, u5)
+	}
+
+	if u1.DeepEqual(u6) {
+		t.Errorf("%v and %v should not be equal\n", u1, u6)
+	}
+
+	if u6.DeepEqual(u7) {
+		t.Errorf("%v and %v should not be equal\n", u6, u7)
+	}
 }