Skip to content

Commit

Permalink
handle batch EVM requests (#84)
Browse files Browse the repository at this point in the history
* add decodeRequestMiddleware & batch request decoding

* progress towards batch request processing

* hackily overriding headers fixes response

* refactor, headers still incorrect

* handle passing response headers

* handle empty batch requests

* rename & add docs to batchResponseWriter

* make batch requests concurrent

* cleanup & add doc comments to all funcs

* unit test the cache state header response

* fix default CORs header for cache misses

* fix cache hit counting

* reduce debug log noise

* add happy path testing for batch evm requests

* lol, don't fail b/c it is 4PM

* pass through error statuses!

* test more unhappy paths

* update missing .env vars, misc cleanup

* fix CI docker for e2e tests

* replace request logging mdw w/ decode request mdw

* update middleware sequence documentation

* add ProxyMaximumBatchSize configuration

proxy service responds 413 if it receives a request with >ProxyMaximumBatchSize sub-requests

* onErrStatus(status) -> onErrorHandler(status, body)

* update error handler to include current response headers

* refactor metrics tests to use require.Eventually

instead of arbitrarily sleeping and praying that the metric was created,
wait for the metrics to be created. if the expected metrics are not
created, the test will fail with a timeout.
  • Loading branch information
pirtleshell committed Feb 8, 2024
1 parent 1683d4e commit d7f1fa5
Show file tree
Hide file tree
Showing 20 changed files with 861 additions and 122 deletions.
6 changes: 5 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ PROXY_CONTAINER_DEBUG_PORT=2345
PROXY_HOST_DEBUG_PORT=2345

##### E2E Testing Config
TEST_UNCONFIGURED_PROXY_PORT=7779
TEST_UNCONFIGURED_PROXY_URL=http://localhost:7779
TEST_PROXY_SERVICE_EVM_RPC_URL=http://localhost:7777
TEST_PROXY_SERVICE_EVM_RPC_HOSTNAME=localhost:7777
TEST_PROXY_SERVICE_EVM_RPC_PRUNING_URL=http://localhost:7778
Expand All @@ -56,7 +58,7 @@ TEST_REDIS_ENDPOINT_URL=localhost:6379
##### Kava Proxy Config
# What port the proxy service listens on
PROXY_SERVICE_PORT=7777
LOG_LEVEL=TRACE
LOG_LEVEL=DEBUG
HTTP_READ_TIMEOUT_SECONDS=30
HTTP_WRITE_TIMEOUT_SECONDS=60
# Address of the origin server to proxy all requests to
Expand All @@ -66,6 +68,8 @@ PROXY_BACKEND_HOST_URL_MAP=localhost:7777>http://kava-validator:8545,localhost:7
# otherwise, it falls back to the value in PROXY_BACKEND_HOST_URL_MAP
PROXY_HEIGHT_BASED_ROUTING_ENABLED=true
PROXY_PRUNING_BACKEND_HOST_URL_MAP=localhost:7777>http://kava-pruning:8545,localhost:7778>http://kava-pruning:8545
# PROXY_MAXIMUM_REQ_BATCH_SIZE is a proxy-enforced limit on the number of subrequest in a batch
PROXY_MAXIMUM_REQ_BATCH_SIZE=100
# Configuration for the servcie to connect to it's database
DATABASE_NAME=postgres
DATABASE_ENDPOINT_URL=postgres:5432
Expand Down
60 changes: 31 additions & 29 deletions architecture/MIDDLEWARE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -20,49 +20,51 @@ Any modifications that the middleware function makes to the request or response

The earlier the middleware is instantiated, the later it will run. For example the first middleware created by the proxy service is the middleware that will run after the request has been logged and proxied, thereby allowing it to access both the recorded request body and response body, and any context enrichment added by prior middleware.

```golang
service := ProxyService{}
https://github.com/Kava-Labs/kava-proxy-service/blob/847d7889bf5f37770d373d73cd4600a769ebd29c/service/service.go#L54-L110

// create an http router for registering handlers for a given route
mux := http.NewServeMux()
## Middleware

// will run after the proxy middleware handler and is
// the final function called after all other middleware
// allowing it to access values added to the request context
// to do things like metric the response and cache the response
afterProxyFinalizer := createAfterProxyFinalizer(&service)
The middleware sequence of EVM requests to the proxy service:
![Middleware Sequence for Proxy Service](./images/proxy_service_middleware_sequence.jpg)

// create an http handler that will proxy any request to the specified URL
proxyMiddleware := createProxyRequestMiddleware(afterProxyFinalizer, config, serviceLogger)
### Decode Request Middleware

// create an http handler that will log the request to stdout
// this handler will run before the proxyMiddleware handler
requestLoggingMiddleware := createRequestLoggingMiddleware(proxyMiddleware, serviceLogger)
1. Captures start time of request for latency metrics calculations
1. Attempts to decode the request:
* As a single EVM request. If successful, forwards to Single Request Middleware Sequence with the request in the context.
* As a batch EVM request. If successful, forwards to Batch Processing Middleware with batch in context.
* On failure to decode, the request is sent down the Single Request Middleware Sequence, but with nothing in the context.

// register middleware chain as the default handler for any request to the proxy service
mux.HandleFunc("/", requestLoggingMiddleware)
### Single Request Middleware Sequence
If a single request is decoded (as opposed to a batch list), or the request fails to decode, it is forwarded down this middleware sequence. Additionally, each individual sub-request of a batch is routed through this sequence in order to leverage caching and metrics collection.

// create an http server for the caller to start on demand with a call to ProxyService.Run()
server := &http.Server{
Addr: fmt.Sprintf(":%s", config.ProxyServicePort),
Handler: mux,
}
```
This middleware sequence uses the decoded single request from the request context.

## Middleware
#### IsCached Middleware
The front part of the two-part caching middleware. Responsible for determining if an incoming request can be fielded from the cache. If it can, the cached response is put into the context.

### Request Logging Middleware
See [CACHING](./CACHING.md#iscachedmiddleware) for more details.

1. Logs the request body to stdout and stores a parsed version of the request body in the context key `X-KAVA-PROXY-DECODED-REQUEST-BODY` for use by other middleware.

### Proxy Middleware
#### Proxy Middleware

1. Proxies the request to the configured backend origin server.
2. Times the roundtrip latency for the response from the backend origin server and stores the latency in the context key `X-KAVA-PROXY-ORIGIN-ROUNDTRIP-LATENCY-MILLISECONDS` for use by other middleware.

1. Times the roundtrip latency for the response from the backend origin server and stores the latency in the context key `X-KAVA-PROXY-ORIGIN-ROUNDTRIP-LATENCY-MILLISECONDS` for use by other middleware.
The Proxy middleware is responsible for writing the response to the requestor. Subsequent middlewares are non-blocking to the response.

See [Proxy Routing](./PROXY_ROUTING.md) for details on configuration and how requests are routed.

### After Proxy Middleware
#### Caching Middleware
Handles determining if a response can be put in the cache if it isn't already.

See [CACHING](./CACHING.md#cachingmiddleware) for more details.

#### After Proxy Middleware

1. Parses the request body and latency from context key values and creates a request metric for the proxied request.

### Batch Processing Middleware
1. Pulls decoded batch out of the request context
2. Separates into individual sub-requests
3. Routes each sub-request through the Single Request Middleware Sequence in order to leverage caching and metrics creation.
4. Combines all sub-request responses into a single response to the client.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ci.docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ services:
EVM_QUERY_SERVICE_URL: https://evmrpc.internal.testnet.proxy.kava.io
ports:
- "${PROXY_HOST_PORT}:${PROXY_CONTAINER_PORT}"
- "${TEST_UNCONFIGURED_PROXY_PORT}:${PROXY_CONTAINER_PORT}"
- "${PROXY_CONTAINER_EVM_RPC_PRUNING_PORT}:${PROXY_CONTAINER_PORT}"
- "${PROXY_HOST_DEBUG_PORT}:${PROXY_CONTAINER_DEBUG_PORT}"
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Config struct {
EnableHeightBasedRouting bool
ProxyPruningBackendHostURLMapRaw string
ProxyPruningBackendHostURLMap map[string]url.URL
ProxyMaximumBatchSize int
EvmQueryServiceURL string
DatabaseName string
DatabaseEndpointURL string
Expand Down Expand Up @@ -64,6 +65,8 @@ const (
PROXY_BACKEND_HOST_URL_MAP_ENVIRONMENT_KEY = "PROXY_BACKEND_HOST_URL_MAP"
PROXY_HEIGHT_BASED_ROUTING_ENABLED_KEY = "PROXY_HEIGHT_BASED_ROUTING_ENABLED"
PROXY_PRUNING_BACKEND_HOST_URL_MAP_ENVIRONMENT_KEY = "PROXY_PRUNING_BACKEND_HOST_URL_MAP"
PROXY_MAXIMUM_BATCH_SIZE_ENVIRONMENT_KEY = "PROXY_MAXIMUM_REQ_BATCH_SIZE"
DEFAULT_PROXY_MAXIMUM_BATCH_SIZE = 500
PROXY_SERVICE_PORT_ENVIRONMENT_KEY = "PROXY_SERVICE_PORT"
DATABASE_NAME_ENVIRONMENT_KEY = "DATABASE_NAME"
DATABASE_ENDPOINT_URL_ENVIRONMENT_KEY = "DATABASE_ENDPOINT_URL"
Expand Down Expand Up @@ -279,6 +282,7 @@ func ReadConfig() Config {
EnableHeightBasedRouting: EnvOrDefaultBool(PROXY_HEIGHT_BASED_ROUTING_ENABLED_KEY, false),
ProxyPruningBackendHostURLMapRaw: rawProxyPruningBackendHostURLMap,
ProxyPruningBackendHostURLMap: parsedProxyPruningBackendHostURLMap,
ProxyMaximumBatchSize: EnvOrDefaultInt(PROXY_MAXIMUM_BATCH_SIZE_ENVIRONMENT_KEY, DEFAULT_PROXY_MAXIMUM_BATCH_SIZE),
DatabaseName: os.Getenv(DATABASE_NAME_ENVIRONMENT_KEY),
DatabaseEndpointURL: os.Getenv(DATABASE_ENDPOINT_URL_ENVIRONMENT_KEY),
DatabaseUserName: os.Getenv(DATABASE_USERNAME_ENVIRONMENT_KEY),
Expand Down
7 changes: 7 additions & 0 deletions decode/evm_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@ func DecodeEVMRPCRequest(body []byte) (*EVMRPCRequestEnvelope, error) {
return &request, err
}

// DecodeEVMRPCRequest attempts to decode raw bytes to a list of EVMRPCRequestEnvelopes
func DecodeEVMRPCRequestList(body []byte) ([]*EVMRPCRequestEnvelope, error) {
var request []*EVMRPCRequestEnvelope
err := json.Unmarshal(body, &request)
return request, err
}

// ExtractBlockNumberFromEVMRPCRequest attempts to extract the block number
// associated with a request if
// - the request is a valid evm rpc request
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:

# run redis for proxy service to cache responses
redis:
image: 'bitnami/redis:latest'
image: "bitnami/redis:latest"
env_file: .env
ports:
- "${REDIS_HOST_PORT}:${REDIS_CONTAINER_PORT}"
Expand Down Expand Up @@ -59,6 +59,7 @@ services:
ports:
- "${PROXY_HOST_PORT}:${PROXY_CONTAINER_PORT}"
- "${PROXY_CONTAINER_EVM_RPC_PRUNING_PORT}:${PROXY_CONTAINER_PORT}"
- "${TEST_UNCONFIGURED_PROXY_PORT}:${PROXY_CONTAINER_PORT}"
- "${PROXY_HOST_DEBUG_PORT}:${PROXY_CONTAINER_DEBUG_PORT}"
cap_add:
- SYS_PTRACE # Allows for attaching debugger to process in this container
Loading

0 comments on commit d7f1fa5

Please sign in to comment.